@sayknow-cli/coding-agent 0.3.0 → 0.3.2
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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +2 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/defaults/skc-defaults.d.ts +2 -13
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +9 -1
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +133 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/threaded-inbound.d.ts +19 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +25 -0
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +14 -10
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/fast-help.ts +30 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/notify-cli.ts +152 -5
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/commands/team.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +55 -7
- package/src/config/settings-schema.ts +30 -1
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/defaults/skc-defaults.ts +2 -27
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/hooks/skill-state.ts +57 -0
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +56 -11
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/event-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +53 -11
- package/src/modes/interactive-mode.ts +50 -3
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config.ts +23 -2
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +417 -45
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +984 -242
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/threaded-inbound.ts +60 -4
- package/src/notifications/threaded-render.ts +20 -2
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +248 -125
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +85 -22
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +25 -8
- package/src/skc-runtime/ultragoal-runtime.ts +75 -15
- package/src/skc-runtime/workflow-manifest.generated.json +29 -0
- package/src/skc-runtime/workflow-manifest.ts +7 -2
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/fetch.ts +94 -1
- package/src/tools/index.ts +6 -1
- package/src/tools/telegram-send.ts +137 -0
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +159 -0
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct MCP server registration CLI helpers.
|
|
3
|
+
*
|
|
4
|
+
* This surface only writes explicit user-provided server definitions to SKC's
|
|
5
|
+
* own MCP config. It never imports or inherits live configs from other agents.
|
|
6
|
+
*/
|
|
7
|
+
import { getMCPConfigPath, getProjectDir } from "@sayknow-cli/utils";
|
|
8
|
+
import { getMCPServer, readMCPConfigFile, removeMCPServer, upsertMCPServer } from "../runtime-mcp/config-writer";
|
|
9
|
+
import type { MCPConfigFile, MCPServerConfig } from "../runtime-mcp/types";
|
|
10
|
+
|
|
11
|
+
export type MCPAction = "add" | "list" | "remove";
|
|
12
|
+
|
|
13
|
+
export interface MCPCommandArgs {
|
|
14
|
+
action: MCPAction;
|
|
15
|
+
name?: string;
|
|
16
|
+
commandArgs?: string[];
|
|
17
|
+
flags: {
|
|
18
|
+
project?: boolean;
|
|
19
|
+
force?: boolean;
|
|
20
|
+
json?: boolean;
|
|
21
|
+
type?: "stdio" | "http" | "sse";
|
|
22
|
+
command?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
arg?: string[];
|
|
25
|
+
env?: string[];
|
|
26
|
+
header?: string[];
|
|
27
|
+
cwd?: string;
|
|
28
|
+
timeout?: number;
|
|
29
|
+
};
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MCPArgsError extends Error {}
|
|
34
|
+
|
|
35
|
+
interface ScopedPath {
|
|
36
|
+
scope: "user" | "project";
|
|
37
|
+
path: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface RedactedServerEntry {
|
|
41
|
+
name: string;
|
|
42
|
+
config: MCPServerConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const REDACTED = "<redacted>";
|
|
46
|
+
const SENSITIVE_KEY_PATTERN =
|
|
47
|
+
/(?:token|secret|key|credential|password|passwd|pwd|authorization|auth|bearer|cookie|session)/i;
|
|
48
|
+
|
|
49
|
+
function resolvePath(args: MCPCommandArgs): ScopedPath {
|
|
50
|
+
const scope = args.flags.project ? "project" : "user";
|
|
51
|
+
return { scope, path: getMCPConfigPath(scope, args.cwd ?? getProjectDir()) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parsePairs(values: string[] | undefined, label: string): Record<string, string> | undefined {
|
|
55
|
+
if (!values || values.length === 0) return undefined;
|
|
56
|
+
const parsed: Record<string, string> = {};
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
const index = value.indexOf("=");
|
|
59
|
+
if (index <= 0) {
|
|
60
|
+
throw new MCPArgsError(`Invalid ${label}. Use KEY=VALUE.`);
|
|
61
|
+
}
|
|
62
|
+
const key = value.slice(0, index).trim();
|
|
63
|
+
if (!key) {
|
|
64
|
+
throw new MCPArgsError(`Invalid ${label}. Key cannot be empty.`);
|
|
65
|
+
}
|
|
66
|
+
parsed[key] = value.slice(index + 1);
|
|
67
|
+
}
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildServerConfig(args: MCPCommandArgs): MCPServerConfig {
|
|
72
|
+
const type = args.flags.type ?? (args.flags.url ? "http" : "stdio");
|
|
73
|
+
const timeout = args.flags.timeout;
|
|
74
|
+
const shared = timeout === undefined ? {} : { timeout };
|
|
75
|
+
|
|
76
|
+
if (type === "stdio") {
|
|
77
|
+
const command = args.flags.command ?? args.commandArgs?.[0];
|
|
78
|
+
if (!command) {
|
|
79
|
+
throw new MCPArgsError("`skc mcp add` requires --command <cmd> or a positional command for stdio servers.");
|
|
80
|
+
}
|
|
81
|
+
const config: MCPServerConfig = {
|
|
82
|
+
...shared,
|
|
83
|
+
type: "stdio",
|
|
84
|
+
command,
|
|
85
|
+
};
|
|
86
|
+
const positionalArgs = args.flags.command ? [] : (args.commandArgs ?? []).slice(1);
|
|
87
|
+
const serverArgs = [...positionalArgs, ...(args.flags.arg ?? [])];
|
|
88
|
+
if (serverArgs.length > 0) config.args = serverArgs;
|
|
89
|
+
const env = parsePairs(args.flags.env, "env");
|
|
90
|
+
if (env) config.env = env;
|
|
91
|
+
if (args.flags.cwd) config.cwd = args.flags.cwd;
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const url = args.flags.url ?? args.commandArgs?.[0];
|
|
96
|
+
if (!url) {
|
|
97
|
+
throw new MCPArgsError(`\`skc mcp add --type ${type}\` requires --url <url> or a positional URL.`);
|
|
98
|
+
}
|
|
99
|
+
const headers = parsePairs(args.flags.header, "header");
|
|
100
|
+
if (type === "http") {
|
|
101
|
+
const config: MCPServerConfig = {
|
|
102
|
+
...shared,
|
|
103
|
+
type,
|
|
104
|
+
url,
|
|
105
|
+
};
|
|
106
|
+
if (headers) config.headers = headers;
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
const config: MCPServerConfig = {
|
|
110
|
+
...shared,
|
|
111
|
+
type,
|
|
112
|
+
url,
|
|
113
|
+
};
|
|
114
|
+
if (headers) config.headers = headers;
|
|
115
|
+
return config;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function redactRecord(
|
|
119
|
+
record: Record<string, string> | undefined,
|
|
120
|
+
redactAllValues: boolean,
|
|
121
|
+
): Record<string, string> | undefined {
|
|
122
|
+
if (!record) return undefined;
|
|
123
|
+
return Object.fromEntries(
|
|
124
|
+
Object.entries(record).map(([key, value]) => [
|
|
125
|
+
key,
|
|
126
|
+
redactAllValues || SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : value,
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function redactMCPServerConfig(config: MCPServerConfig): MCPServerConfig {
|
|
132
|
+
const redacted = { ...config } as MCPServerConfig;
|
|
133
|
+
if ("env" in redacted) {
|
|
134
|
+
const env = redactRecord(redacted.env, true);
|
|
135
|
+
if (env) redacted.env = env;
|
|
136
|
+
}
|
|
137
|
+
if ("headers" in redacted) {
|
|
138
|
+
const headers = redactRecord(redacted.headers, true);
|
|
139
|
+
if (headers) redacted.headers = headers;
|
|
140
|
+
}
|
|
141
|
+
if (redacted.auth) {
|
|
142
|
+
redacted.auth = {
|
|
143
|
+
type: redacted.auth.type,
|
|
144
|
+
credentialId: redacted.auth.credentialId ? REDACTED : undefined,
|
|
145
|
+
tokenUrl: redacted.auth.tokenUrl,
|
|
146
|
+
clientId: redacted.auth.clientId ? REDACTED : undefined,
|
|
147
|
+
clientSecret: redacted.auth.clientSecret ? REDACTED : undefined,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (redacted.oauth) {
|
|
151
|
+
redacted.oauth = {
|
|
152
|
+
clientId: redacted.oauth.clientId ? REDACTED : undefined,
|
|
153
|
+
clientSecret: redacted.oauth.clientSecret ? REDACTED : undefined,
|
|
154
|
+
redirectUri: redacted.oauth.redirectUri,
|
|
155
|
+
callbackPort: redacted.oauth.callbackPort,
|
|
156
|
+
callbackPath: redacted.oauth.callbackPath,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return redacted;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collectEntries(config: MCPConfigFile): RedactedServerEntry[] {
|
|
163
|
+
return Object.entries(config.mcpServers ?? {})
|
|
164
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
165
|
+
.map(([name, serverConfig]) => ({ name, config: redactMCPServerConfig(serverConfig) }));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeJson(value: unknown): void {
|
|
169
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderServerLine(entry: RedactedServerEntry): string {
|
|
173
|
+
const config = entry.config;
|
|
174
|
+
if (config.type === "http" || config.type === "sse") {
|
|
175
|
+
return `${entry.name}\t${config.type}\t${config.url}`;
|
|
176
|
+
}
|
|
177
|
+
const args = config.args && config.args.length > 0 ? ` ${config.args.join(" ")}` : "";
|
|
178
|
+
return `${entry.name}\tstdio\t${config.command}${args}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderDetails(entry: RedactedServerEntry): string {
|
|
182
|
+
return `${renderServerLine(entry)}\n${JSON.stringify(entry.config, null, 2)}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function runAdd(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
|
|
186
|
+
if (!args.name) throw new MCPArgsError("`skc mcp add` requires a server name.");
|
|
187
|
+
const config = buildServerConfig(args);
|
|
188
|
+
const result = await upsertMCPServer(scoped.path, args.name, config, { force: args.flags.force });
|
|
189
|
+
const redacted = redactMCPServerConfig(config);
|
|
190
|
+
if (args.flags.json) {
|
|
191
|
+
writeJson({
|
|
192
|
+
action: "add",
|
|
193
|
+
status: result.status,
|
|
194
|
+
name: args.name,
|
|
195
|
+
scope: scoped.scope,
|
|
196
|
+
path: scoped.path,
|
|
197
|
+
config: redacted,
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (result.status === "skipped") {
|
|
202
|
+
process.stdout.write(
|
|
203
|
+
`MCP server "${args.name}" already exists in ${scoped.scope} config. Pass --force to overwrite.\n`,
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
process.stdout.write(`MCP server "${args.name}" ${result.status} in ${scoped.scope} config: ${scoped.path}\n`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function runList(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
|
|
211
|
+
const config = await readMCPConfigFile(scoped.path);
|
|
212
|
+
const entries = collectEntries(config);
|
|
213
|
+
if (args.flags.json) {
|
|
214
|
+
writeJson({ action: "list", scope: scoped.scope, path: scoped.path, servers: entries });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (entries.length === 0) {
|
|
218
|
+
process.stdout.write(`No MCP servers registered in ${scoped.scope} config: ${scoped.path}\n`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(`MCP servers in ${scoped.scope} config: ${scoped.path}\n`);
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
process.stdout.write(`${renderDetails(entry)}\n`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runRemove(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
|
|
228
|
+
if (!args.name) throw new MCPArgsError("`skc mcp remove` requires a server name.");
|
|
229
|
+
const existing = await getMCPServer(scoped.path, args.name);
|
|
230
|
+
if (!existing) {
|
|
231
|
+
throw new MCPArgsError(`MCP server "${args.name}" not found in ${scoped.scope} config.`);
|
|
232
|
+
}
|
|
233
|
+
await removeMCPServer(scoped.path, args.name);
|
|
234
|
+
const entry = { name: args.name, config: redactMCPServerConfig(existing) };
|
|
235
|
+
if (args.flags.json) {
|
|
236
|
+
writeJson({
|
|
237
|
+
action: "remove",
|
|
238
|
+
status: "removed",
|
|
239
|
+
name: args.name,
|
|
240
|
+
scope: scoped.scope,
|
|
241
|
+
path: scoped.path,
|
|
242
|
+
removed: entry,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
process.stdout.write(`Removed MCP server "${args.name}" from ${scoped.scope} config: ${scoped.path}\n`);
|
|
247
|
+
process.stdout.write(`${renderDetails(entry)}\n`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function runMCPCommand(args: MCPCommandArgs): Promise<void> {
|
|
251
|
+
const scoped = resolvePath(args);
|
|
252
|
+
try {
|
|
253
|
+
switch (args.action) {
|
|
254
|
+
case "add":
|
|
255
|
+
await runAdd(args, scoped);
|
|
256
|
+
return;
|
|
257
|
+
case "list":
|
|
258
|
+
await runList(args, scoped);
|
|
259
|
+
return;
|
|
260
|
+
case "remove":
|
|
261
|
+
await runRemove(args, scoped);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof MCPArgsError) {
|
|
266
|
+
process.stderr.write(`${error.message}\n`);
|
|
267
|
+
process.exitCode = 2;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface NotifyCommandDeps {
|
|
|
29
29
|
pollIntervalMs?: number;
|
|
30
30
|
setupChatId?: string;
|
|
31
31
|
setupRedact?: boolean;
|
|
32
|
+
setupInteractive?: boolean;
|
|
33
|
+
threadedModePrompt?: (message: string) => Promise<string>;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface TelegramApiResponse<T> {
|
|
@@ -47,6 +49,18 @@ interface TelegramUpdate {
|
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
interface TelegramUser {
|
|
53
|
+
id: number;
|
|
54
|
+
is_bot?: boolean;
|
|
55
|
+
first_name?: string;
|
|
56
|
+
username?: string;
|
|
57
|
+
has_topics_enabled?: boolean;
|
|
58
|
+
allows_users_to_create_topics?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ThreadedModeState = "enabled" | "disabled" | "unknown";
|
|
62
|
+
type ThreadedModeFinalLabel = "verified" | "unverified" | "unknown";
|
|
63
|
+
|
|
50
64
|
const DEFAULT_API_BASE = "https://api.telegram.org";
|
|
51
65
|
const DEFAULT_POLL_TIMEOUT_MS = 60_000;
|
|
52
66
|
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
@@ -120,7 +134,11 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
|
120
134
|
throw new Error("Telegram bot token is required.");
|
|
121
135
|
}
|
|
122
136
|
|
|
123
|
-
await
|
|
137
|
+
const user = await getMe(fetchImpl, apiBase, token);
|
|
138
|
+
const threadedState = await verifyThreadedMode(fetchImpl, apiBase, token, user, {
|
|
139
|
+
interactive: resolveSetupInteractive(deps),
|
|
140
|
+
prompt: deps.threadedModePrompt ?? promptForThreadedMode,
|
|
141
|
+
});
|
|
124
142
|
process.stdout.write(
|
|
125
143
|
"Token validated. Message your bot now from the private Telegram chat to pair notifications.\n",
|
|
126
144
|
);
|
|
@@ -145,7 +163,9 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
|
145
163
|
if (deps.setupRedact) settings.set("notifications.redact", true);
|
|
146
164
|
await settings.flush();
|
|
147
165
|
|
|
148
|
-
process.stdout.write(
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`Notifications enabled. botToken=${maskToken(token)} chatId=${chatId} threaded=${threadedLabel(threadedState)}\n`,
|
|
168
|
+
);
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
async function promptForToken(): Promise<string> {
|
|
@@ -160,14 +180,135 @@ async function promptForToken(): Promise<string> {
|
|
|
160
180
|
}
|
|
161
181
|
}
|
|
162
182
|
|
|
183
|
+
const THREADED_ENABLED_SUCCESS =
|
|
184
|
+
"Telegram Threaded Mode capability verified for this bot. SKC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with a one-time nudge.\n";
|
|
185
|
+
|
|
186
|
+
const THREADED_MISSING_WARNING =
|
|
187
|
+
"Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue; update Telegram/Bot API support or re-run setup if per-session topics fail.\n";
|
|
188
|
+
|
|
189
|
+
const THREADED_NONINTERACTIVE_WARNING =
|
|
190
|
+
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive, but per-session Telegram delivery may fail closed until the bot owner enables Threaded Mode in @BotFather. SKC cannot enable it through the Bot API.\n";
|
|
191
|
+
|
|
192
|
+
const THREADED_DISABLED_GUIDANCE =
|
|
193
|
+
"Telegram Threaded Mode is OFF for this bot. SKC needs Telegram private-chat topics so each session can use its own thread.\n" +
|
|
194
|
+
"SKC cannot enable this through the Bot API. Open @BotFather, select this bot, enable Threaded Mode / forum topics for private chats, then return here.\n" +
|
|
195
|
+
"Telegram may require an additional Stars purchase fee for private-chat topics.\n";
|
|
196
|
+
|
|
197
|
+
const THREADED_DISABLED_PROMPT =
|
|
198
|
+
"Press Enter after enabling Threaded Mode, or type skip to finish setup with a warning: ";
|
|
199
|
+
|
|
200
|
+
const THREADED_STILL_OFF = "Telegram still reports Threaded Mode OFF for this bot.\n";
|
|
201
|
+
|
|
202
|
+
const THREADED_RETRY_PROMPT = "Press Enter to check again, or type skip to finish setup with a warning: ";
|
|
203
|
+
|
|
204
|
+
const THREADED_SKIP_WARNING =
|
|
205
|
+
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved, but per-session Telegram delivery may fail closed until Threaded Mode is enabled in BotFather.\n";
|
|
206
|
+
|
|
207
|
+
const THREADED_INVALID_INPUT = "Type Enter to retry or skip to continue with a warning.\n";
|
|
208
|
+
|
|
209
|
+
const THREADED_RETRY_INPUTS = new Set(["", "y", "yes", "r", "retry"]);
|
|
210
|
+
const THREADED_SKIP_INPUTS = new Set(["s", "skip", "n", "no"]);
|
|
211
|
+
|
|
212
|
+
function isTelegramUser(value: unknown): value is TelegramUser {
|
|
213
|
+
return Boolean(value) && typeof value === "object" && typeof (value as { id?: unknown }).id === "number";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function getMe(fetchImpl: typeof fetch, apiBase: string, token: string): Promise<TelegramUser> {
|
|
217
|
+
const user = await callTelegram<unknown>(fetchImpl, apiBase, token, "getMe", {});
|
|
218
|
+
if (!isTelegramUser(user)) {
|
|
219
|
+
throw new Error("Telegram getMe returned invalid Telegram response: missing valid User result.");
|
|
220
|
+
}
|
|
221
|
+
return user;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function threadedModeState(user: TelegramUser): ThreadedModeState {
|
|
225
|
+
if (user.has_topics_enabled === true) return "enabled";
|
|
226
|
+
if (user.has_topics_enabled === false) return "disabled";
|
|
227
|
+
return "unknown";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function threadedLabel(state: ThreadedModeState): ThreadedModeFinalLabel {
|
|
231
|
+
if (state === "enabled") return "verified";
|
|
232
|
+
if (state === "disabled") return "unverified";
|
|
233
|
+
return "unknown";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveSetupInteractive(deps: NotifyCommandDeps): boolean {
|
|
237
|
+
if (deps.setupInteractive !== undefined) return deps.setupInteractive;
|
|
238
|
+
return Boolean(process.stdin.isTTY) && !deps.setupChatId?.trim();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function promptForThreadedMode(message: string): Promise<string> {
|
|
242
|
+
if (!process.stdin.isTTY) return "skip";
|
|
243
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
244
|
+
try {
|
|
245
|
+
return (await rl.question(message)).trim();
|
|
246
|
+
} finally {
|
|
247
|
+
rl.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function verifyThreadedMode(
|
|
252
|
+
fetchImpl: typeof fetch,
|
|
253
|
+
apiBase: string,
|
|
254
|
+
token: string,
|
|
255
|
+
initialUser: TelegramUser,
|
|
256
|
+
opts: { interactive: boolean; prompt: (message: string) => Promise<string> },
|
|
257
|
+
): Promise<ThreadedModeState> {
|
|
258
|
+
const classify = (user: TelegramUser): ThreadedModeState | undefined => {
|
|
259
|
+
const state = threadedModeState(user);
|
|
260
|
+
if (state === "enabled") {
|
|
261
|
+
process.stdout.write(THREADED_ENABLED_SUCCESS);
|
|
262
|
+
return "enabled";
|
|
263
|
+
}
|
|
264
|
+
if (state === "unknown") {
|
|
265
|
+
process.stdout.write(THREADED_MISSING_WARNING);
|
|
266
|
+
return "unknown";
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const initial = classify(initialUser);
|
|
272
|
+
if (initial) return initial;
|
|
273
|
+
|
|
274
|
+
if (!opts.interactive) {
|
|
275
|
+
process.stdout.write(THREADED_NONINTERACTIVE_WARNING);
|
|
276
|
+
return "disabled";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
process.stdout.write(THREADED_DISABLED_GUIDANCE);
|
|
280
|
+
let firstPrompt = true;
|
|
281
|
+
for (;;) {
|
|
282
|
+
const answer = (await opts.prompt(firstPrompt ? THREADED_DISABLED_PROMPT : THREADED_RETRY_PROMPT))
|
|
283
|
+
.trim()
|
|
284
|
+
.toLowerCase();
|
|
285
|
+
firstPrompt = false;
|
|
286
|
+
if (THREADED_SKIP_INPUTS.has(answer)) {
|
|
287
|
+
process.stdout.write(THREADED_SKIP_WARNING);
|
|
288
|
+
return "disabled";
|
|
289
|
+
}
|
|
290
|
+
if (!THREADED_RETRY_INPUTS.has(answer)) {
|
|
291
|
+
process.stdout.write(THREADED_INVALID_INPUT);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const resolved = classify(await getMe(fetchImpl, apiBase, token));
|
|
295
|
+
if (resolved) return resolved;
|
|
296
|
+
process.stdout.write(THREADED_STILL_OFF);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
163
300
|
async function runStatus(deps: NotifyCommandDeps): Promise<void> {
|
|
164
301
|
const settings = await getSettings(deps);
|
|
165
302
|
const cfg = getNotificationConfig(settings);
|
|
166
303
|
process.stdout.write(
|
|
167
304
|
`${chalk.bold("Notifications")}\n` +
|
|
168
305
|
` enabled: ${cfg.enabled}\n` +
|
|
169
|
-
` botToken: ${maskToken(cfg.botToken)}\n` +
|
|
170
|
-
` chatId: ${cfg.chatId ?? "(unset)"}\n` +
|
|
306
|
+
` telegram.botToken: ${maskToken(cfg.botToken)}\n` +
|
|
307
|
+
` telegram.chatId: ${cfg.chatId ?? "(unset)"}\n` +
|
|
308
|
+
` discord.botToken: ${maskToken(cfg.discord.botToken)}\n` +
|
|
309
|
+
` discord.channelId: ${cfg.discord.channelId ?? "(unset)"}\n` +
|
|
310
|
+
` slack.botToken: ${maskToken(cfg.slack.botToken)}\n` +
|
|
311
|
+
` slack.channelId: ${cfg.slack.channelId ?? "(unset)"}\n` +
|
|
171
312
|
` redact: ${cfg.redact}\n`,
|
|
172
313
|
);
|
|
173
314
|
}
|
|
@@ -263,12 +404,18 @@ ${chalk.bold("Usage:")}
|
|
|
263
404
|
${APP_NAME} notify status
|
|
264
405
|
|
|
265
406
|
${chalk.bold("Subcommands:")}
|
|
266
|
-
setup Pair a Telegram bot token with a private chat
|
|
407
|
+
setup Pair a Telegram bot token with a private chat and verify Threaded Mode capability
|
|
267
408
|
status Show notification configuration without secrets
|
|
268
409
|
|
|
269
410
|
${chalk.bold("Examples:")}
|
|
270
411
|
${APP_NAME} notify setup
|
|
271
412
|
${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
|
|
272
413
|
${APP_NAME} notify status
|
|
414
|
+
|
|
415
|
+
${chalk.bold("Threaded Mode:")}
|
|
416
|
+
SKC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|
|
417
|
+
capability via getMe.has_topics_enabled. If it is off, enable Threaded Mode in @BotFather;
|
|
418
|
+
bots cannot toggle it through the Bot API. If Telegram refuses topic creation at runtime,
|
|
419
|
+
SKC delivers flat to the paired private chat and nudges you to enable Threaded Mode.
|
|
273
420
|
`);
|
|
274
421
|
}
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getPluginsCacheDir,
|
|
17
17
|
MarketplaceManager,
|
|
18
18
|
} from "../extensibility/plugins/marketplace/index.js";
|
|
19
|
+
import { installSkcPluginBundle, isSkcPluginBundleSource, readRegistry } from "../extensibility/skc-plugins";
|
|
19
20
|
import { type ScanReport, scanPluginDir } from "../extensibility/plugins/security-scanner";
|
|
20
21
|
import { theme } from "../modes/theme/theme";
|
|
21
22
|
|
|
@@ -50,6 +51,8 @@ export interface PluginCommandArgs {
|
|
|
50
51
|
disable?: string;
|
|
51
52
|
set?: string;
|
|
52
53
|
scope?: "user" | "project";
|
|
54
|
+
user?: boolean;
|
|
55
|
+
project?: boolean;
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -111,6 +114,10 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
|
|
|
111
114
|
result.flags.dryRun = true;
|
|
112
115
|
} else if (arg === "-l" || arg === "--local") {
|
|
113
116
|
result.flags.local = true;
|
|
117
|
+
} else if (arg === "--user") {
|
|
118
|
+
result.flags.user = true;
|
|
119
|
+
} else if (arg === "--project") {
|
|
120
|
+
result.flags.project = true;
|
|
114
121
|
} else if (arg === "--enable" && i + 1 < args.length) {
|
|
115
122
|
result.flags.enable = args[++i];
|
|
116
123
|
} else if (arg === "--disable" && i + 1 < args.length) {
|
|
@@ -366,7 +373,14 @@ function printSecurityAdvisory(report: ScanReport): void {
|
|
|
366
373
|
async function handleInstall(
|
|
367
374
|
manager: PluginManager,
|
|
368
375
|
packages: string[],
|
|
369
|
-
flags: {
|
|
376
|
+
flags: {
|
|
377
|
+
json?: boolean;
|
|
378
|
+
force?: boolean;
|
|
379
|
+
dryRun?: boolean;
|
|
380
|
+
scope?: "user" | "project";
|
|
381
|
+
user?: boolean;
|
|
382
|
+
project?: boolean;
|
|
383
|
+
},
|
|
370
384
|
): Promise<void> {
|
|
371
385
|
if (packages.length === 0) {
|
|
372
386
|
console.error(chalk.red(`Usage: ${APP_NAME} plugin install <package[@version]>[features] ...`));
|
|
@@ -381,6 +395,32 @@ async function handleInstall(
|
|
|
381
395
|
const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name));
|
|
382
396
|
|
|
383
397
|
for (const spec of packages) {
|
|
398
|
+
// SKC plugin bundle classifier: a source containing sayknow-plugin.json (or a
|
|
399
|
+
// git/tarball source) routes to the bundle installer BEFORE marketplace/npm.
|
|
400
|
+
if (await isSkcPluginBundleSource(spec)) {
|
|
401
|
+
if (flags.user === flags.project) {
|
|
402
|
+
console.error(
|
|
403
|
+
chalk.red(`SKC plugin bundle install requires exactly one of --user or --project for "${spec}".`),
|
|
404
|
+
);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const scope: "user" | "project" = flags.user ? "user" : "project";
|
|
408
|
+
try {
|
|
409
|
+
const res = await installSkcPluginBundle(spec, { scope, cwd: process.cwd(), force: flags.force });
|
|
410
|
+
if (flags.json) {
|
|
411
|
+
console.log(JSON.stringify({ name: res.entry.name, status: res.status, scope }, null, 2));
|
|
412
|
+
} else {
|
|
413
|
+
console.log(
|
|
414
|
+
chalk.green(`${theme.status.success} ${res.status} SKC plugin ${res.entry.name} (${scope})`),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error(chalk.red(`${theme.status.error} Failed to install SKC plugin ${spec}: ${err}`));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
384
424
|
const target = classifyInstallTarget(spec, knownMarketplaces);
|
|
385
425
|
|
|
386
426
|
if (target.type === "marketplace") {
|
|
@@ -519,13 +559,16 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
|
|
|
519
559
|
const npmPlugins = await manager.list();
|
|
520
560
|
const mktMgr = await makeMarketplaceManager();
|
|
521
561
|
const mktPlugins = await mktMgr.listInstalledPlugins();
|
|
562
|
+
const cwd = getProjectDir();
|
|
563
|
+
const [skcUser, skcProject] = await Promise.all([readRegistry("user", cwd), readRegistry("project", cwd)]);
|
|
564
|
+
const skcBundles = [...skcUser.plugins, ...skcProject.plugins];
|
|
522
565
|
|
|
523
566
|
if (flags.json) {
|
|
524
|
-
console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins }, null, 2));
|
|
567
|
+
console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins, skc: skcBundles }, null, 2));
|
|
525
568
|
return;
|
|
526
569
|
}
|
|
527
570
|
|
|
528
|
-
if (npmPlugins.length === 0 && mktPlugins.length === 0) {
|
|
571
|
+
if (npmPlugins.length === 0 && mktPlugins.length === 0 && skcBundles.length === 0) {
|
|
529
572
|
console.log(chalk.dim("No plugins installed"));
|
|
530
573
|
console.log(chalk.dim(`\nInstall plugins with: ${APP_NAME} plugin install <package>`));
|
|
531
574
|
return;
|
|
@@ -567,6 +610,26 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
|
|
|
567
610
|
console.log(` ${plugin.id} (${version})${scopeLabel}${shadowLabel}`);
|
|
568
611
|
}
|
|
569
612
|
}
|
|
613
|
+
|
|
614
|
+
if (skcBundles.length > 0) {
|
|
615
|
+
if (npmPlugins.length > 0 || mktPlugins.length > 0) console.log();
|
|
616
|
+
console.log(chalk.bold("SKC Plugin Bundles:\n"));
|
|
617
|
+
for (const plugin of skcBundles) {
|
|
618
|
+
const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
|
|
619
|
+
const scopeLabel = chalk.dim(` (${plugin.scope})`);
|
|
620
|
+
const disabledCount = plugin.disabledSurfaceIds.length;
|
|
621
|
+
const quarantineCount = plugin.quarantine?.length ?? 0;
|
|
622
|
+
const detail = [
|
|
623
|
+
disabledCount > 0 ? `${disabledCount} disabled` : null,
|
|
624
|
+
quarantineCount > 0 ? `${quarantineCount} quarantined` : null,
|
|
625
|
+
]
|
|
626
|
+
.filter((v): v is string => Boolean(v))
|
|
627
|
+
.join(", ");
|
|
628
|
+
console.log(
|
|
629
|
+
`${status} ${plugin.name}@${plugin.version}${scopeLabel}${detail ? chalk.dim(` — ${detail}`) : ""}`,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
570
633
|
}
|
|
571
634
|
|
|
572
635
|
async function handleLink(manager: PluginManager, paths: string[], flags: { json?: boolean }): Promise<void> {
|