@oh-my-pi/pi-coding-agent 3.4.1337 → 3.5.1337
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 +20 -0
- package/package.json +4 -4
- package/src/core/sdk.ts +14 -1
- package/src/core/settings-manager.ts +33 -0
- package/src/core/system-prompt.ts +15 -0
- package/src/core/title-generator.ts +28 -6
- package/src/core/tools/index.ts +6 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +297 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +477 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/settings-defs.ts +2 -31
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/interactive-mode.ts +24 -296
- package/src/modes/print-mode.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.5.1337] - 2026-01-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added session header and footer output in text mode showing version, model, provider, thinking level, and session ID
|
|
10
|
+
- Added Extension Control Center dashboard accessible via `/extensions` command for unified management of all providers and extensions
|
|
11
|
+
- Added ability to enable/disable individual extensions with persistent settings
|
|
12
|
+
- Added three-column dashboard layout with sidebar tree, extension list, and inspector panel
|
|
13
|
+
- Added fuzzy search filtering for extensions in the dashboard
|
|
14
|
+
- Added keyboard navigation with Tab to cycle panes, j/k for navigation, Space to toggle, Enter to expand/collapse
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Redesigned Extension Control Center from 3-column layout to tabbed interface with horizontal provider tabs and 2-column grid
|
|
19
|
+
- Replaced sidebar tree navigation with provider tabs using TAB/Shift+TAB cycling
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed title generation flag not resetting when starting a new session
|
|
24
|
+
|
|
5
25
|
## [3.4.1337] - 2026-01-03
|
|
6
26
|
|
|
7
27
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1337",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.5.1337",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.5.1337",
|
|
44
|
+
"@oh-my-pi/pi-tui": "3.5.1337",
|
|
45
45
|
"@sinclair/typebox": "^0.34.46",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"chalk": "^5.5.0",
|
package/src/core/sdk.ts
CHANGED
|
@@ -79,8 +79,10 @@ import {
|
|
|
79
79
|
createLsTool,
|
|
80
80
|
createReadOnlyTools,
|
|
81
81
|
createReadTool,
|
|
82
|
+
createRulebookTool,
|
|
82
83
|
createWriteTool,
|
|
83
84
|
editTool,
|
|
85
|
+
filterRulebookRules,
|
|
84
86
|
findTool,
|
|
85
87
|
grepTool,
|
|
86
88
|
lsTool,
|
|
@@ -604,7 +606,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
604
606
|
const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
|
|
605
607
|
time("discoverSkills");
|
|
606
608
|
|
|
607
|
-
// Discover
|
|
609
|
+
// Discover rules
|
|
608
610
|
const ttsrManager = createTtsrManager(settingsManager.getTtsrSettings());
|
|
609
611
|
const rulesResult = loadCapability<Rule>(ruleCapability.id, { cwd });
|
|
610
612
|
for (const rule of rulesResult.items) {
|
|
@@ -614,6 +616,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
614
616
|
}
|
|
615
617
|
time("discoverTtsrRules");
|
|
616
618
|
|
|
619
|
+
// Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
|
|
620
|
+
const rulebookRules = filterRulebookRules(rulesResult.items);
|
|
621
|
+
time("filterRulebookRules");
|
|
622
|
+
|
|
617
623
|
const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
|
|
618
624
|
time("discoverContextFiles");
|
|
619
625
|
|
|
@@ -757,6 +763,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
757
763
|
|
|
758
764
|
let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
|
|
759
765
|
|
|
766
|
+
// Add rulebook tool if there are rules with descriptions (always enabled, regardless of --tools)
|
|
767
|
+
if (rulebookRules.length > 0) {
|
|
768
|
+
allToolsArray.push(createRulebookTool(rulebookRules));
|
|
769
|
+
}
|
|
770
|
+
|
|
760
771
|
// Filter out hidden tools unless explicitly requested
|
|
761
772
|
if (options.explicitTools) {
|
|
762
773
|
const explicitSet = new Set(options.explicitTools);
|
|
@@ -781,6 +792,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
781
792
|
cwd,
|
|
782
793
|
skills,
|
|
783
794
|
contextFiles,
|
|
795
|
+
rulebookRules,
|
|
784
796
|
});
|
|
785
797
|
time("buildSystemPrompt");
|
|
786
798
|
|
|
@@ -791,6 +803,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
791
803
|
cwd,
|
|
792
804
|
skills,
|
|
793
805
|
contextFiles,
|
|
806
|
+
rulebookRules,
|
|
794
807
|
customPrompt: options.systemPrompt,
|
|
795
808
|
});
|
|
796
809
|
} else {
|
|
@@ -105,6 +105,7 @@ export interface Settings {
|
|
|
105
105
|
edit?: EditSettings;
|
|
106
106
|
ttsr?: TtsrSettings;
|
|
107
107
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
108
|
+
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -594,6 +595,38 @@ export class SettingsManager {
|
|
|
594
595
|
this.save();
|
|
595
596
|
}
|
|
596
597
|
|
|
598
|
+
getDisabledExtensions(): string[] {
|
|
599
|
+
return [...(this.settings.disabledExtensions ?? [])];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
setDisabledExtensions(extensionIds: string[]): void {
|
|
603
|
+
this.globalSettings.disabledExtensions = extensionIds;
|
|
604
|
+
this.save();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
isExtensionEnabled(extensionId: string): boolean {
|
|
608
|
+
return !(this.settings.disabledExtensions ?? []).includes(extensionId);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
enableExtension(extensionId: string): void {
|
|
612
|
+
const disabled = this.globalSettings.disabledExtensions ?? [];
|
|
613
|
+
const index = disabled.indexOf(extensionId);
|
|
614
|
+
if (index !== -1) {
|
|
615
|
+
disabled.splice(index, 1);
|
|
616
|
+
this.globalSettings.disabledExtensions = disabled;
|
|
617
|
+
this.save();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
disableExtension(extensionId: string): void {
|
|
622
|
+
const disabled = this.globalSettings.disabledExtensions ?? [];
|
|
623
|
+
if (!disabled.includes(extensionId)) {
|
|
624
|
+
disabled.push(extensionId);
|
|
625
|
+
this.globalSettings.disabledExtensions = disabled;
|
|
626
|
+
this.save();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
597
630
|
getTtsrSettings(): TtsrSettings {
|
|
598
631
|
return this.settings.ttsr ?? {};
|
|
599
632
|
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { contextFileCapability } from "../capability/context-file";
|
|
8
|
+
import type { Rule } from "../capability/rule";
|
|
8
9
|
import { systemPromptCapability } from "../capability/system-prompt";
|
|
9
10
|
import { getDocsPath, getExamplesPath, getReadmePath } from "../config";
|
|
10
11
|
import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
|
|
11
12
|
import type { SkillsSettings } from "./settings-manager";
|
|
12
13
|
import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
|
|
13
14
|
import type { ToolName } from "./tools/index";
|
|
15
|
+
import { formatRulesForPrompt } from "./tools/rulebook";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Execute a git command synchronously and return stdout or null on failure.
|
|
@@ -238,6 +240,8 @@ export interface BuildSystemPromptOptions {
|
|
|
238
240
|
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
|
239
241
|
/** Pre-loaded skills (skips discovery if provided). */
|
|
240
242
|
skills?: Skill[];
|
|
243
|
+
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
244
|
+
rulebookRules?: Rule[];
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -250,6 +254,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
250
254
|
cwd,
|
|
251
255
|
contextFiles: providedContextFiles,
|
|
252
256
|
skills: providedSkills,
|
|
257
|
+
rulebookRules,
|
|
253
258
|
} = options;
|
|
254
259
|
const resolvedCwd = cwd ?? process.cwd();
|
|
255
260
|
const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
|
|
@@ -310,6 +315,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
310
315
|
prompt += formatSkillsForPrompt(skills);
|
|
311
316
|
}
|
|
312
317
|
|
|
318
|
+
// Append rules section (always enabled when rules exist)
|
|
319
|
+
if (rulebookRules && rulebookRules.length > 0) {
|
|
320
|
+
prompt += formatRulesForPrompt(rulebookRules);
|
|
321
|
+
}
|
|
322
|
+
|
|
313
323
|
// Add date/time and working directory last
|
|
314
324
|
prompt += `\nCurrent date and time: ${dateTime}`;
|
|
315
325
|
prompt += `\nCurrent working directory: ${resolvedCwd}`;
|
|
@@ -419,6 +429,11 @@ Documentation:
|
|
|
419
429
|
prompt += formatSkillsForPrompt(skills);
|
|
420
430
|
}
|
|
421
431
|
|
|
432
|
+
// Append rules section (always enabled when rules exist)
|
|
433
|
+
if (rulebookRules && rulebookRules.length > 0) {
|
|
434
|
+
prompt += formatRulesForPrompt(rulebookRules);
|
|
435
|
+
}
|
|
436
|
+
|
|
422
437
|
// Add date/time and working directory last
|
|
423
438
|
prompt += `\nCurrent date and time: ${dateTime}`;
|
|
424
439
|
prompt += `\nCurrent working directory: ${resolvedCwd}`;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { logger } from "./logger";
|
|
7
8
|
import type { ModelRegistry } from "./model-registry";
|
|
8
9
|
import { findSmolModel } from "./model-resolver";
|
|
9
10
|
|
|
@@ -43,21 +44,35 @@ export async function generateSessionTitle(
|
|
|
43
44
|
savedSmolModel?: string,
|
|
44
45
|
): Promise<string | null> {
|
|
45
46
|
const model = await findTitleModel(registry, savedSmolModel);
|
|
46
|
-
if (!model)
|
|
47
|
+
if (!model) {
|
|
48
|
+
logger.debug("title-generator: no smol model found");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
47
51
|
|
|
48
52
|
const apiKey = await registry.getApiKey(model);
|
|
49
|
-
if (!apiKey)
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
logger.debug("title-generator: no API key for model", { provider: model.provider, id: model.id });
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
// Truncate message if too long
|
|
52
59
|
const truncatedMessage =
|
|
53
60
|
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
|
|
54
61
|
|
|
62
|
+
const request = {
|
|
63
|
+
model: `${model.provider}/${model.id}`,
|
|
64
|
+
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
65
|
+
userMessage: `<user-message>\n${truncatedMessage}\n</user-message>`,
|
|
66
|
+
maxTokens: 30,
|
|
67
|
+
};
|
|
68
|
+
logger.debug("title-generator: request", request);
|
|
69
|
+
|
|
55
70
|
try {
|
|
56
71
|
const response = await completeSimple(
|
|
57
72
|
model,
|
|
58
73
|
{
|
|
59
|
-
systemPrompt:
|
|
60
|
-
messages: [{ role: "user", content:
|
|
74
|
+
systemPrompt: request.systemPrompt,
|
|
75
|
+
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
61
76
|
},
|
|
62
77
|
{
|
|
63
78
|
apiKey,
|
|
@@ -74,13 +89,20 @@ export async function generateSessionTitle(
|
|
|
74
89
|
}
|
|
75
90
|
title = title.trim();
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
logger.debug("title-generator: response", {
|
|
93
|
+
title,
|
|
94
|
+
usage: response.usage,
|
|
95
|
+
stopReason: response.stopReason,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!title) {
|
|
78
99
|
return null;
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
// Clean up: remove quotes, trailing punctuation
|
|
82
103
|
return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
|
|
83
|
-
} catch {
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.debug("title-generator: error", { error: err instanceof Error ? err.message : String(err) });
|
|
84
106
|
return null;
|
|
85
107
|
}
|
|
86
108
|
}
|
package/src/core/tools/index.ts
CHANGED
|
@@ -24,6 +24,12 @@ export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./no
|
|
|
24
24
|
export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
|
|
25
25
|
export { createReadTool, type ReadToolDetails, readTool } from "./read";
|
|
26
26
|
export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
|
|
27
|
+
export {
|
|
28
|
+
createRulebookTool,
|
|
29
|
+
filterRulebookRules,
|
|
30
|
+
formatRulesForPrompt,
|
|
31
|
+
type RulebookToolDetails,
|
|
32
|
+
} from "./rulebook";
|
|
27
33
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
28
34
|
export type { TruncationResult } from "./truncate";
|
|
29
35
|
export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rulebook Tool
|
|
3
|
+
*
|
|
4
|
+
* Allows the agent to fetch full content of rules that have descriptions.
|
|
5
|
+
* Rules are listed in the system prompt with name + description; this tool
|
|
6
|
+
* retrieves the complete rule content on demand.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import type { Rule } from "../../capability/rule";
|
|
12
|
+
|
|
13
|
+
export interface RulebookToolDetails {
|
|
14
|
+
type: "rulebook";
|
|
15
|
+
ruleName: string;
|
|
16
|
+
found: boolean;
|
|
17
|
+
content?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rulebookSchema = Type.Object({
|
|
21
|
+
name: Type.String({ description: "The name of the rule to fetch" }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a rulebook tool with access to discovered rules.
|
|
26
|
+
* @param rules - Array of discovered rules (non-TTSR rules with descriptions)
|
|
27
|
+
*/
|
|
28
|
+
export function createRulebookTool(rules: Rule[]): AgentTool<typeof rulebookSchema> {
|
|
29
|
+
// Build lookup map for O(1) access
|
|
30
|
+
const ruleMap = new Map<string, Rule>();
|
|
31
|
+
for (const rule of rules) {
|
|
32
|
+
ruleMap.set(rule.name, rule);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ruleNames = rules.map((r) => r.name);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: "rulebook",
|
|
39
|
+
label: "Rulebook",
|
|
40
|
+
description: `Fetch the full content of a project rule by name. Use this when a rule listed in <available_rules> is relevant to your current task. Available: ${ruleNames.join(", ") || "(none)"}`,
|
|
41
|
+
parameters: rulebookSchema,
|
|
42
|
+
execute: async (_toolCallId: string, { name }: { name: string }) => {
|
|
43
|
+
const rule = ruleMap.get(name);
|
|
44
|
+
|
|
45
|
+
if (!rule) {
|
|
46
|
+
const available = ruleNames.join(", ");
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: `Rule "${name}" not found. Available rules: ${available || "(none)"}` }],
|
|
49
|
+
details: {
|
|
50
|
+
type: "rulebook",
|
|
51
|
+
ruleName: name,
|
|
52
|
+
found: false,
|
|
53
|
+
} satisfies RulebookToolDetails,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: `# Rule: ${rule.name}\n\n${rule.content}` }],
|
|
59
|
+
details: {
|
|
60
|
+
type: "rulebook",
|
|
61
|
+
ruleName: name,
|
|
62
|
+
found: true,
|
|
63
|
+
content: rule.content,
|
|
64
|
+
} satisfies RulebookToolDetails,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Filter rules to only those suitable for the rulebook (have descriptions, no TTSR trigger).
|
|
72
|
+
*/
|
|
73
|
+
export function filterRulebookRules(rules: Rule[]): Rule[] {
|
|
74
|
+
return rules.filter((rule) => {
|
|
75
|
+
// Exclude TTSR rules (handled separately by streaming)
|
|
76
|
+
if (rule.ttsrTrigger) return false;
|
|
77
|
+
// Exclude always-apply rules (already in context)
|
|
78
|
+
if (rule.alwaysApply) return false;
|
|
79
|
+
// Must have a description for agent to know when to fetch
|
|
80
|
+
if (!rule.description) return false;
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format rules for inclusion in the system prompt.
|
|
87
|
+
* Lists rule names and descriptions so the agent knows what's available.
|
|
88
|
+
*/
|
|
89
|
+
export function formatRulesForPrompt(rules: Rule[]): string {
|
|
90
|
+
if (rules.length === 0) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lines = [
|
|
95
|
+
"\n\n## Available Rules",
|
|
96
|
+
"",
|
|
97
|
+
"The following project rules are available. Use the `rulebook` tool to fetch a rule's full content when it's relevant to your task.",
|
|
98
|
+
"",
|
|
99
|
+
"<available_rules>",
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const rule of rules) {
|
|
103
|
+
lines.push(" <rule>");
|
|
104
|
+
lines.push(` <name>${escapeXml(rule.name)}</name>`);
|
|
105
|
+
lines.push(` <description>${escapeXml(rule.description || "")}</description>`);
|
|
106
|
+
if (rule.globs && rule.globs.length > 0) {
|
|
107
|
+
lines.push(` <globs>${escapeXml(rule.globs.join(", "))}</globs>`);
|
|
108
|
+
}
|
|
109
|
+
lines.push(" </rule>");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("</available_rules>");
|
|
113
|
+
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function escapeXml(str: string): string {
|
|
118
|
+
return str
|
|
119
|
+
.replace(/&/g, "&")
|
|
120
|
+
.replace(/</g, "<")
|
|
121
|
+
.replace(/>/g, ">")
|
|
122
|
+
.replace(/"/g, """)
|
|
123
|
+
.replace(/'/g, "'");
|
|
124
|
+
}
|