decorated-pi 0.2.2 → 0.4.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/README.md +82 -74
- package/extensions/file-times.ts +124 -0
- package/extensions/guidance.ts +5 -3
- package/extensions/index.ts +6 -2
- package/extensions/io.ts +587 -0
- package/extensions/lsp/client.ts +181 -428
- package/extensions/lsp/env.ts +45 -12
- package/extensions/lsp/format.ts +102 -237
- package/extensions/lsp/index.ts +8 -11
- package/extensions/lsp/manager.ts +249 -0
- package/extensions/lsp/prompt.ts +3 -42
- package/extensions/lsp/protocol.ts +219 -0
- package/extensions/lsp/servers.ts +80 -160
- package/extensions/lsp/tools.ts +175 -510
- package/extensions/lsp/types.ts +42 -0
- package/extensions/mcp/builtin.ts +126 -0
- package/extensions/mcp/client.ts +106 -0
- package/extensions/mcp/index.ts +123 -0
- package/extensions/{extend-model.ts → model-integration.ts} +127 -4
- package/extensions/patch.ts +842 -0
- package/extensions/providers/ark-coding.ts +2 -0
- package/extensions/safety/detect.ts +78 -707
- package/extensions/safety/entropy.ts +226 -0
- package/extensions/safety/index.ts +44 -97
- package/extensions/safety/patterns.ts +155 -0
- package/extensions/safety/types.ts +50 -0
- package/extensions/settings.ts +10 -0
- package/extensions/slash.ts +165 -9
- package/extensions/smart-at.ts +339 -111
- package/extensions/subdir-agents.ts +43 -13
- package/package.json +3 -4
- package/tsconfig.json +16 -0
- package/extensions/lsp/server-manager.ts +0 -309
- package/extensions/lsp/trust.ts +0 -45
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety Detection — known secret patterns and safe-pattern exclusions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { basename, extname } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
type SecretPattern,
|
|
8
|
+
type ConfigStringEntry,
|
|
9
|
+
CONFIG_FILE_EXTENSIONS,
|
|
10
|
+
CONFIG_BASENAME_REGEX,
|
|
11
|
+
SENSITIVE_CONFIG_KEY_REGEX,
|
|
12
|
+
PLACEHOLDER_VALUE_REGEX,
|
|
13
|
+
CONFIG_VALUE_MIN_LENGTH,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
// ─── High-confidence Secret Patterns (40+ known formats) ─────────────────
|
|
17
|
+
|
|
18
|
+
export const SECRET_PATTERNS: SecretPattern[] = [
|
|
19
|
+
// AWS
|
|
20
|
+
{ name: "AWS Access Key ID", pattern: /AKIA[0-9A-Z]{16}/, minLength: 16, allowsSpaces: false, highConfidence: true },
|
|
21
|
+
{ name: "AWS Secret Access Key", pattern: /(?:aws)?_?(?:secret)?_?(?:access)?_?key['"\s:=]+['"]?[0-9a-zA-Z/+]{40}['"]?/i, minLength: 30, allowsSpaces: false, highConfidence: true },
|
|
22
|
+
// GitHub
|
|
23
|
+
{ name: "GitHub OAuth Token", pattern: /gho_[0-9a-zA-Z]{36}/, minLength: 36, allowsSpaces: false, highConfidence: true },
|
|
24
|
+
{ name: "GitHub App Token", pattern: /(?:ghu|ghs)_[0-9a-zA-Z]{36}/, minLength: 36, allowsSpaces: false, highConfidence: true },
|
|
25
|
+
{ name: "GitHub PAT", pattern: /ghp_[0-9a-zA-Z]{36}/, minLength: 36, allowsSpaces: false, highConfidence: true },
|
|
26
|
+
{ name: "GitHub Fine-Grained Token", pattern: /github_pat_[0-9a-zA-Z_]{22,}/, minLength: 26, allowsSpaces: false, highConfidence: true },
|
|
27
|
+
// GitLab
|
|
28
|
+
{ name: "GitLab PAT", pattern: /glpat-[0-9a-zA-Z\-_]{20,}/, minLength: 20, allowsSpaces: false, highConfidence: true },
|
|
29
|
+
{ name: "GitLab Runner Token", pattern: /glrt-[0-9a-zA-Z_\-]{20,}/, minLength: 20, allowsSpaces: false, highConfidence: true },
|
|
30
|
+
// Slack
|
|
31
|
+
{ name: "Slack Token", pattern: /xox[baprs]-[0-9a-zA-Z\-]{10,48}/, minLength: 15, allowsSpaces: false, highConfidence: true },
|
|
32
|
+
{ name: "Slack Webhook URL", pattern: /https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8,}\/B[a-zA-Z0-9_]{8,}\/[a-zA-Z0-9_]{24}/, minLength: 60, allowsSpaces: false, highConfidence: true },
|
|
33
|
+
// JWT
|
|
34
|
+
{ name: "JSON Web Token", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/, minLength: 36, allowsSpaces: false, highConfidence: true },
|
|
35
|
+
// Google
|
|
36
|
+
{ name: "Google API Key", pattern: /AIza[0-9A-Za-z\-_]{35}/, minLength: 35, allowsSpaces: false, highConfidence: true },
|
|
37
|
+
{ name: "Google OAuth Token", pattern: /ya29\.[0-9A-Za-z\-_]+/, minLength: 10, allowsSpaces: false, highConfidence: true },
|
|
38
|
+
// Stripe
|
|
39
|
+
{ name: "Stripe Secret Key", pattern: /sk_live_[0-9a-zA-Z]{24,}/, minLength: 24, allowsSpaces: false, highConfidence: true },
|
|
40
|
+
{ name: "Stripe Restricted Key", pattern: /rk_live_[0-9a-zA-Z]{24,}/, minLength: 24, allowsSpaces: false, highConfidence: true },
|
|
41
|
+
// Twilio / SendGrid / Discord
|
|
42
|
+
{ name: "Twilio API Key", pattern: /SK[a-z0-9]{32}/, minLength: 30, allowsSpaces: false, highConfidence: true },
|
|
43
|
+
{ name: "SendGrid API Key", pattern: /SG\.[a-zA-Z0-9_-]{22,}\.[a-zA-Z0-9_-]{40,}/, minLength: 40, allowsSpaces: false, highConfidence: true },
|
|
44
|
+
{ name: "Discord Bot Token", pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/, minLength: 40, allowsSpaces: false, highConfidence: true },
|
|
45
|
+
// OpenAI / Anthropic / Volcengine Ark
|
|
46
|
+
{ name: "OpenAI API Key", pattern: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/, minLength: 40, allowsSpaces: false, highConfidence: true },
|
|
47
|
+
{ name: "OpenAI API Key (New)", pattern: /sk-(?:proj-)?[a-zA-Z0-9\-_]{40,}/, minLength: 40, allowsSpaces: false, highConfidence: true },
|
|
48
|
+
{ name: "Anthropic API Key", pattern: /sk-ant-api[0-9]{2}-[a-zA-Z0-9\-_]{80,}/, minLength: 80, allowsSpaces: false, highConfidence: true },
|
|
49
|
+
{ name: "Volcengine Ark API Key", pattern: /ark-[a-zA-Z0-9\-_]{20,}/, minLength: 20, allowsSpaces: false, highConfidence: true },
|
|
50
|
+
// NPM / PyPI
|
|
51
|
+
{ name: "NPM Token", pattern: /npm_[a-zA-Z0-9]{36}/, minLength: 36, allowsSpaces: false, highConfidence: true },
|
|
52
|
+
{ name: "PyPI Token", pattern: /pypi-[a-zA-Z0-9_\-]{50,}/, minLength: 50, allowsSpaces: false, highConfidence: true },
|
|
53
|
+
// Private Keys
|
|
54
|
+
{ name: "RSA Private Key", pattern: /-----BEGIN RSA PRIVATE KEY-----\r?\n(?:[A-Za-z0-9+/=]+\r?\n)+-----END RSA PRIVATE KEY-----/, minLength: 40, allowsSpaces: true, highConfidence: true },
|
|
55
|
+
{ name: "OpenSSH Private Key", pattern: /-----BEGIN OPENSSH PRIVATE KEY-----\r?\n(?:[A-Za-z0-9+/=]+\r?\n)+-----END OPENSSH PRIVATE KEY-----/, minLength: 40, allowsSpaces: true, highConfidence: true },
|
|
56
|
+
{ name: "EC Private Key", pattern: /-----BEGIN EC PRIVATE KEY-----\r?\n(?:[A-Za-z0-9+/=]+\r?\n)+-----END EC PRIVATE KEY-----/, minLength: 40, allowsSpaces: true, highConfidence: true },
|
|
57
|
+
{ name: "PGP Private Key", pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----\r?\n(?:[A-Za-z0-9+/=]+\r?\n)+-----END PGP PRIVATE KEY BLOCK-----/, minLength: 40, allowsSpaces: true, highConfidence: true },
|
|
58
|
+
{ name: "Generic Private Key", pattern: /-----BEGIN (ENCRYPTED )?PRIVATE KEY-----\r?\n(?:[A-Za-z0-9+/=]+\r?\n)+-----END \1PRIVATE KEY-----/, minLength: 40, allowsSpaces: true, highConfidence: true },
|
|
59
|
+
// Database URIs
|
|
60
|
+
{ name: "MongoDB Connection String", pattern: /mongodb(?:\+srv)?:\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/, minLength: 20, allowsSpaces: false, highConfidence: true },
|
|
61
|
+
{ name: "PostgreSQL Connection String", pattern: /postgres(?:ql)?:\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/, minLength: 20, allowsSpaces: false, highConfidence: true },
|
|
62
|
+
{ name: "MySQL Connection String", pattern: /mysql:\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/, minLength: 20, allowsSpaces: false, highConfidence: true },
|
|
63
|
+
{ name: "Redis Connection String", pattern: /redis:\/\/[^\s'"]*:[^\s'"]+@[^\s'"]+/, minLength: 15, allowsSpaces: false, highConfidence: true },
|
|
64
|
+
// URL-embedded passwords
|
|
65
|
+
{ name: "Password in URL", pattern: /[a-zA-Z]{3,10}:\/\/[^/\s:@]{3,20}:[^/\s:@]{3,20}@[^\s'"]+/, minLength: 15, allowsSpaces: false, highConfidence: true },
|
|
66
|
+
// Generic assignments (lower confidence — checked against SAFE_PATTERNS)
|
|
67
|
+
{ name: "Bearer Token", pattern: /[Bb]earer\s+[a-zA-Z0-9\-._~+/]+=*/, minLength: 15, allowsSpaces: false, highConfidence: false },
|
|
68
|
+
{ name: "Basic Auth Header", pattern: /[Bb]asic\s+[a-zA-Z0-9+/]{20,}={0,2}/, minLength: 20, allowsSpaces: false, highConfidence: false },
|
|
69
|
+
{ name: "API Key Assignment", pattern: /(?:api[_-]?key|apikey|api[_-]?secret)['"\s:=]+['"]?[a-zA-Z0-9\-._]{20,}['"]?/i, minLength: 20, allowsSpaces: false, highConfidence: false },
|
|
70
|
+
{ name: "Secret Assignment", pattern: /(?:secret|token|password|passwd|pwd)['"\s:=]+['"]?[a-zA-Z0-9\-._!@#$%^&*]{8,}['"]?/i, minLength: 12, allowsSpaces: false, highConfidence: false },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// ─── Safe Patterns (false-positive exclusion) ────────────────────────────
|
|
74
|
+
|
|
75
|
+
export const SAFE_PATTERNS: RegExp[] = [
|
|
76
|
+
/^https?:\/\/[a-zA-Z0-9.-]+(?:\/[a-zA-Z0-9.\/_\-?&=#%]*)?$/, // URLs without credentials
|
|
77
|
+
/^\.\.?\/[a-zA-Z0-9_\-./]+$/, // Relative file paths
|
|
78
|
+
/^\/[a-zA-Z0-9_\-./]+$/, // Absolute Unix paths
|
|
79
|
+
/^[a-zA-Z]:\\[a-zA-Z0-9_\-\\./]+$/, // Windows paths
|
|
80
|
+
/^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/, // Email addresses
|
|
81
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, // UUIDs
|
|
82
|
+
/^v?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?(?:\+[a-zA-Z0-9.]+)?$/, // Semver
|
|
83
|
+
/^(?:xxx+|your[_-]?(?:api[_-]?)?key|placeholder|example|test|demo|sample)/i, // Placeholders
|
|
84
|
+
/^[0-9a-f]{40}$/i, // Git SHA-1
|
|
85
|
+
/^[0-9a-f]{64}$/i, // SHA-256
|
|
86
|
+
/^@[a-z0-9-]+\/[a-z0-9-]+$/, // npm scoped packages
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export function isSafeContent(content: string): boolean {
|
|
90
|
+
for (const pat of SAFE_PATTERNS) {
|
|
91
|
+
if (pat.test(content)) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Config-file detection ───────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export function isConfigLikeFile(filePath?: string): boolean {
|
|
99
|
+
if (!filePath) return false;
|
|
100
|
+
const name = basename(filePath);
|
|
101
|
+
if (CONFIG_BASENAME_REGEX.test(name)) return true;
|
|
102
|
+
return CONFIG_FILE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const CONFIG_STRING_PATTERNS: RegExp[] = [
|
|
106
|
+
/(?<key>"[^"\r\n]+"|'[^'\r\n]+'|[A-Za-z0-9_.-]+)\s*[:=]\s*"(?<value>(?:\\.|[^"\\])*)"/g,
|
|
107
|
+
/(?<key>"[^"\r\n]+"|'[^'\r\n]+'|[A-Za-z0-9_.-]+)\s*[:=]\s*'(?<value>(?:\\.|[^'\\])*)'/g,
|
|
108
|
+
/(?<key>[A-Za-z0-9_.-]+)\s*=\s*(?<value>[^\r\n#;]+)/g,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
export function normalizeConfigKey(key: string): string {
|
|
112
|
+
return key
|
|
113
|
+
.trim()
|
|
114
|
+
.replace(/^['"]|['"]$/g, "")
|
|
115
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
116
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/[.\-\s]+/g, "_")
|
|
119
|
+
.replace(/_+/g, "_")
|
|
120
|
+
.replace(/^_+|_+$/g, "");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function looksLikeSensitiveConfigValue(value: string): boolean {
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
if (!trimmed) return false;
|
|
126
|
+
if (PLACEHOLDER_VALUE_REGEX.test(trimmed)) return false;
|
|
127
|
+
if (isSafeContent(trimmed)) return false;
|
|
128
|
+
if (/^(?:true|false|null)$/i.test(trimmed)) return false;
|
|
129
|
+
if (/^[+-]?\d+(?:\.\d+)?$/.test(trimmed)) return false;
|
|
130
|
+
return trimmed.length >= CONFIG_VALUE_MIN_LENGTH;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function extractConfigStringEntries(content: string): ConfigStringEntry[] {
|
|
134
|
+
const entries: ConfigStringEntry[] = [];
|
|
135
|
+
const seen = new Set<string>();
|
|
136
|
+
|
|
137
|
+
for (const pattern of CONFIG_STRING_PATTERNS) {
|
|
138
|
+
for (const match of content.matchAll(pattern)) {
|
|
139
|
+
const key = match.groups?.key;
|
|
140
|
+
const value = match.groups?.value;
|
|
141
|
+
if (!key || value === undefined || match.index === undefined) continue;
|
|
142
|
+
const full = match[0] ?? "";
|
|
143
|
+
const rel = full.indexOf(value);
|
|
144
|
+
if (rel < 0) continue;
|
|
145
|
+
const start = match.index + rel;
|
|
146
|
+
const end = start + value.length;
|
|
147
|
+
const dedupeKey = `${start}-${end}`;
|
|
148
|
+
if (seen.has(dedupeKey)) continue;
|
|
149
|
+
seen.add(dedupeKey);
|
|
150
|
+
entries.push({ key, normalizedKey: normalizeConfigKey(key), value, start, end });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return entries;
|
|
155
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety Detection — shared types and constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ─── Match types ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type SecretMatchSource = "pattern" | "regex" | "entropy";
|
|
8
|
+
|
|
9
|
+
export interface SecretMatch {
|
|
10
|
+
name: string;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
original: string;
|
|
14
|
+
source: SecretMatchSource;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SecretPattern {
|
|
18
|
+
name: string;
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
minLength: number;
|
|
21
|
+
allowsSpaces: boolean;
|
|
22
|
+
/** If true, skip safe-pattern exclusion (unambiguous prefix) */
|
|
23
|
+
highConfidence: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DetectSecretsOptions {
|
|
27
|
+
filePath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Internal types ──────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface ConfigStringEntry {
|
|
33
|
+
key: string;
|
|
34
|
+
normalizedKey: string;
|
|
35
|
+
value: string;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export const MIN_SCAN_LENGTH = 10;
|
|
43
|
+
export const CONFIG_VALUE_MIN_LENGTH = 32;
|
|
44
|
+
export const CONFIG_FILE_EXTENSIONS = new Set([
|
|
45
|
+
".json", ".jsonc", ".env", ".toml", ".yaml", ".yml",
|
|
46
|
+
".ini", ".cfg", ".conf", ".properties",
|
|
47
|
+
]);
|
|
48
|
+
export const CONFIG_BASENAME_REGEX = /^\.env(?:\..+)?$/i;
|
|
49
|
+
export const SENSITIVE_CONFIG_KEY_REGEX = /(?:^|_)(?:apikey|api_(?:key|secret|token)|access_(?:key|token)|refresh_token|client_secret|secret(?:_key)?|private_key|bearer_token|auth(?:orization|_token)?|pass(?:word|wd)?|pwd|token|webhook_secret)(?:_|$)/i;
|
|
50
|
+
export const PLACEHOLDER_VALUE_REGEX = /^(?:\$\{[^}]+\}|\{\{[^}]+\}\}|<[^>]+>|xxx+|placeholder|example|sample|demo|test|changeme|your[_-]?(?:api[_-]?)?key(?:[_-]?here)?)$/i;
|
package/extensions/settings.ts
CHANGED
|
@@ -25,10 +25,17 @@ export interface ProviderCache {
|
|
|
25
25
|
models: ProviderModelEntry[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface McpServerEntry {
|
|
29
|
+
url: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export interface ModuleSettings {
|
|
29
34
|
safety?: boolean;
|
|
30
35
|
lsp?: boolean;
|
|
31
36
|
"smart-at"?: boolean;
|
|
37
|
+
patch?: boolean;
|
|
38
|
+
mcp?: boolean;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export interface DecoratedPiConfig {
|
|
@@ -36,6 +43,7 @@ export interface DecoratedPiConfig {
|
|
|
36
43
|
compactModelKey?: string | null;
|
|
37
44
|
providers?: Record<string, ProviderCache>;
|
|
38
45
|
modules?: ModuleSettings;
|
|
46
|
+
mcpServers?: Record<string, McpServerEntry>;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export function loadConfig(): DecoratedPiConfig {
|
|
@@ -111,6 +119,8 @@ const DEFAULT_MODULES: Required<ModuleSettings> = {
|
|
|
111
119
|
safety: true,
|
|
112
120
|
lsp: true,
|
|
113
121
|
"smart-at": true,
|
|
122
|
+
patch: true,
|
|
123
|
+
mcp: true,
|
|
114
124
|
};
|
|
115
125
|
|
|
116
126
|
export function isModuleEnabled(name: keyof ModuleSettings): boolean {
|
package/extensions/slash.ts
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Slash — 所有扩展命令
|
|
3
3
|
*
|
|
4
4
|
* /dp-model → 模型选择器 (TAB 切换 Image/Compact)
|
|
5
|
-
* /dp-settings → 模块开关 (safety / lsp / smart-at)
|
|
5
|
+
* /dp-settings → 模块开关 (patch / safety / lsp / smart-at)
|
|
6
6
|
* /retry → 中断后继续
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { ModelPickerComponent } from "./
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext, Theme as PiTheme } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { ModelPickerComponent } from "./model-integration.js";
|
|
11
11
|
import { getAllModuleSettings, setModuleEnabled, type ModuleSettings } from "./settings.js";
|
|
12
|
-
import {
|
|
12
|
+
import { getMcpStatus } from "./mcp/index.js";
|
|
13
|
+
import { Container, SettingsList, Spacer, Text, type TUI, type SettingsListTheme, type Component, getKeybindings } from "@earendil-works/pi-tui";
|
|
13
14
|
|
|
14
15
|
// ─── Border component (matches native DynamicBorder) ────────────────────────
|
|
15
16
|
|
|
@@ -60,15 +61,19 @@ function setupDpModelCommand(pi: ExtensionAPI) {
|
|
|
60
61
|
// ─── /dp-settings ──────────────────────────────────────────────────────────
|
|
61
62
|
|
|
62
63
|
const MODULE_LABELS: Record<keyof ModuleSettings, string> = {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
patch: "patch",
|
|
65
|
+
safety: "Secret Redaction",
|
|
66
|
+
lsp: "LSP",
|
|
67
|
+
"smart-at": "@ overload",
|
|
68
|
+
mcp: "MCP",
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
const MODULE_DESCS: Record<keyof ModuleSettings, string> = {
|
|
69
|
-
|
|
72
|
+
patch: "Replace edit/write with patch tool (old_str/new_str replacement + overwrite)",
|
|
73
|
+
safety: "Redact secrets from read / bash output before they enter model context",
|
|
70
74
|
lsp: "Language server diagnostics, hover, definition, references, symbols, rename",
|
|
71
75
|
"smart-at": "Project-aware file search replacing default autocomplete",
|
|
76
|
+
mcp: "MCP client for context7 and exa (zero-config)",
|
|
72
77
|
};
|
|
73
78
|
|
|
74
79
|
class ModuleSettingsComponent extends Container {
|
|
@@ -117,7 +122,7 @@ function setupDpSettingsCommand(pi: ExtensionAPI) {
|
|
|
117
122
|
(tui, theme, _kb, done) =>
|
|
118
123
|
new ModuleSettingsComponent(tui, theme, () => done(undefined))
|
|
119
124
|
);
|
|
120
|
-
ctx.ui.notify("Module settings updated. /reload to apply.", "
|
|
125
|
+
ctx.ui.notify("Module settings updated. /reload to apply.", "warning");
|
|
121
126
|
return;
|
|
122
127
|
}
|
|
123
128
|
ctx.ui.notify("dp-settings requires interactive mode.", "warning");
|
|
@@ -125,6 +130,156 @@ function setupDpSettingsCommand(pi: ExtensionAPI) {
|
|
|
125
130
|
});
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
// ─── /mcp ──────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
class McpStatusComponent extends Container {
|
|
136
|
+
private textComponent: Text;
|
|
137
|
+
private tui: TUI;
|
|
138
|
+
private theme: PiTheme;
|
|
139
|
+
private done: () => void;
|
|
140
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
141
|
+
|
|
142
|
+
constructor(tui: TUI, theme: PiTheme, onDone: () => void) {
|
|
143
|
+
super();
|
|
144
|
+
this.tui = tui;
|
|
145
|
+
this.theme = theme;
|
|
146
|
+
this.done = onDone;
|
|
147
|
+
|
|
148
|
+
this.addChild(new DynamicBorder(theme));
|
|
149
|
+
this.addChild(new Spacer(1));
|
|
150
|
+
|
|
151
|
+
this.textComponent = new Text("", 1, 0);
|
|
152
|
+
this.addChild(this.textComponent);
|
|
153
|
+
|
|
154
|
+
this.addChild(new Spacer(1));
|
|
155
|
+
this.addChild(new Text(this.theme.fg("dim", "Press q to close."), 1, 0));
|
|
156
|
+
this.addChild(new Spacer(1));
|
|
157
|
+
this.addChild(new DynamicBorder(theme));
|
|
158
|
+
|
|
159
|
+
this.refresh();
|
|
160
|
+
|
|
161
|
+
this.timer = setInterval(() => {
|
|
162
|
+
this.refresh();
|
|
163
|
+
const allSettled = getMcpStatus().every((s) => s.state !== "connecting");
|
|
164
|
+
if (allSettled && this.timer) {
|
|
165
|
+
clearInterval(this.timer);
|
|
166
|
+
this.timer = null;
|
|
167
|
+
}
|
|
168
|
+
}, 500);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private refresh() {
|
|
172
|
+
const servers = getMcpStatus();
|
|
173
|
+
|
|
174
|
+
if (servers.length === 0) {
|
|
175
|
+
this.textComponent.setText("No MCP servers configured.");
|
|
176
|
+
this.tui.requestRender();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const connected = servers.filter((s) => s.state === "connected");
|
|
181
|
+
const connecting = servers.filter((s) => s.state === "connecting");
|
|
182
|
+
const failed = servers.filter((s) => s.state === "failed");
|
|
183
|
+
|
|
184
|
+
const lines: string[] = [
|
|
185
|
+
`MCP servers (${servers.length}):`,
|
|
186
|
+
"",
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
for (const s of connected) {
|
|
190
|
+
lines.push(this.theme.fg("accent", `• ${s.name}`) + ` (${s.source})`);
|
|
191
|
+
lines.push(` URL: ${s.url}`);
|
|
192
|
+
lines.push(` Tools: ${s.toolCount}`);
|
|
193
|
+
for (const tool of s.tools) {
|
|
194
|
+
const desc = tool.description ? ` — ${tool.description.slice(0, 60)}` : "";
|
|
195
|
+
lines.push(` - ${tool.name}${desc}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push("");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const s of connecting) {
|
|
201
|
+
lines.push(this.theme.fg("accent", `• ${s.name}`) + ` (${s.source})`);
|
|
202
|
+
lines.push(` URL: ${s.url}`);
|
|
203
|
+
lines.push(` Status: ${this.theme.fg("warning", "connecting...")}`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const s of failed) {
|
|
208
|
+
lines.push(this.theme.fg("accent", `• ${s.name}`) + ` (${s.source})`);
|
|
209
|
+
lines.push(` URL: ${s.url}`);
|
|
210
|
+
lines.push(` Status: ${this.theme.fg("error", "failed")} — ${s.error ?? "unknown error"}`);
|
|
211
|
+
lines.push("");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.textComponent.setText(lines.join("\n"));
|
|
215
|
+
this.tui.requestRender();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handleInput(data: string) {
|
|
219
|
+
const kb = getKeybindings();
|
|
220
|
+
if (
|
|
221
|
+
data === "q" ||
|
|
222
|
+
data === "\r" ||
|
|
223
|
+
data === "\n" ||
|
|
224
|
+
kb.matches(data, "tui.select.cancel")
|
|
225
|
+
) {
|
|
226
|
+
if (this.timer) {
|
|
227
|
+
clearInterval(this.timer);
|
|
228
|
+
this.timer = null;
|
|
229
|
+
}
|
|
230
|
+
this.done();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
dispose() {
|
|
235
|
+
if (this.timer) {
|
|
236
|
+
clearInterval(this.timer);
|
|
237
|
+
this.timer = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function setupMcpCommand(pi: ExtensionAPI) {
|
|
243
|
+
pi.registerCommand("mcp", {
|
|
244
|
+
description: "Show active MCP servers and their tools",
|
|
245
|
+
handler: async (_args, ctx) => {
|
|
246
|
+
if (ctx.hasUI) {
|
|
247
|
+
await ctx.ui.custom<void>(
|
|
248
|
+
(tui, theme, _kb, done) => new McpStatusComponent(tui, theme, () => done(undefined))
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fallback for non-interactive (print / RPC) mode.
|
|
254
|
+
const servers = getMcpStatus();
|
|
255
|
+
if (servers.length === 0) {
|
|
256
|
+
ctx.ui.notify("No MCP servers configured.", "info");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const lines: string[] = [`MCP servers (${servers.length}):`, ""];
|
|
261
|
+
for (const s of servers) {
|
|
262
|
+
lines.push(`• ${s.name} (${s.source})`);
|
|
263
|
+
lines.push(` URL: ${s.url}`);
|
|
264
|
+
if (s.state === "connecting") {
|
|
265
|
+
lines.push(` Status: connecting...`);
|
|
266
|
+
} else if (s.state === "failed") {
|
|
267
|
+
lines.push(` Status: failed — ${s.error ?? "unknown error"}`);
|
|
268
|
+
} else {
|
|
269
|
+
lines.push(` Tools: ${s.toolCount}`);
|
|
270
|
+
for (const tool of s.tools) {
|
|
271
|
+
const desc = tool.description ? ` — ${tool.description.slice(0, 60)}` : "";
|
|
272
|
+
lines.push(` - ${tool.name}${desc}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
lines.push("");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
pi.sendMessage({ customType: "mcp-status", content: lines.join("\n"), display: true }, { triggerTurn: false });
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
128
283
|
// ─── /retry ────────────────────────────────────────────────────────────────
|
|
129
284
|
|
|
130
285
|
function setupRetryCommand(pi: ExtensionAPI) {
|
|
@@ -163,5 +318,6 @@ function setupRetryCommand(pi: ExtensionAPI) {
|
|
|
163
318
|
export function setupSlash(pi: ExtensionAPI) {
|
|
164
319
|
setupDpModelCommand(pi);
|
|
165
320
|
setupDpSettingsCommand(pi);
|
|
321
|
+
setupMcpCommand(pi);
|
|
166
322
|
setupRetryCommand(pi);
|
|
167
323
|
}
|