@zaganjade/pi-multi-skill 1.3.0 → 1.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/src/bundles.ts CHANGED
@@ -1,138 +1,138 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type { SkillBundle } from "./types.ts";
5
- import { parseYamlBundles } from "./yaml-bundles.ts";
6
-
7
- export const DEFAULT_BUNDLES: Record<string, SkillBundle> = {
8
- "bmad-planning": {
9
- description: "BMAD Phase 1-2 — analysis & planning",
10
- skills: ["bmad-master", "analyst", "pm"],
11
- order: "process-first",
12
- default_mode: "meta",
13
- requires: "BMAD Method",
14
- install:
15
- "git clone BMAD-METHOD to ~/.claude/skills/bmad, add path in pi settings \"skills\"",
16
- },
17
- "bmad-solutioning": {
18
- description: "BMAD Phase 3 — architecture & design",
19
- skills: ["bmad-master", "architect", "ux-designer"],
20
- order: "process-first",
21
- default_mode: "meta",
22
- requires: "BMAD Method",
23
- install:
24
- "git clone BMAD-METHOD to ~/.claude/skills/bmad, add path in pi settings \"skills\"",
25
- },
26
- "bmad-build": {
27
- description: "BMAD Phase 4 — story implementation",
28
- skills: ["bmad-master", "developer", "scrum-master"],
29
- order: "process-first",
30
- default_mode: "lazy",
31
- requires: "BMAD Method",
32
- install:
33
- "git clone BMAD-METHOD to ~/.claude/skills/bmad, add path in pi settings \"skills\"",
34
- },
35
- "cc-feature": {
36
- description: "Claude Code-style feature workflow",
37
- skills: [
38
- "using-superpowers",
39
- "brainstorming",
40
- "writing-plans",
41
- "test-driven-development",
42
- "requesting-code-review",
43
- ],
44
- order: "process-first",
45
- default_mode: "lazy",
46
- requires: "Superpowers",
47
- install:
48
- "Install Claude Code Superpowers plugin, or copy skills to ~/.pi/agent/skills",
49
- },
50
- debug: {
51
- description: "Systematic bug investigation",
52
- skills: ["systematic-debugging", "test-driven-development"],
53
- order: "process-first",
54
- default_mode: "full",
55
- requires: "Superpowers",
56
- install:
57
- "Install Claude Code Superpowers plugin, or copy skills to ~/.pi/agent/skills",
58
- },
59
- };
60
-
61
- function getAgentDir(): string {
62
- return join(homedir(), ".pi", "agent");
63
- }
64
-
65
- function mergeBundleFile(
66
- target: Map<string, SkillBundle>,
67
- path: string,
68
- ): void {
69
- try {
70
- const raw = readFileSync(path, "utf-8");
71
- const parsed =
72
- path.endsWith(".yaml") || path.endsWith(".yml")
73
- ? { bundles: parseYamlBundles(raw) }
74
- : (JSON.parse(raw) as { bundles?: Record<string, SkillBundle> });
75
- if (!parsed.bundles) return;
76
- for (const [name, bundle] of Object.entries(parsed.bundles)) {
77
- if (Array.isArray(bundle.skills) && bundle.skills.length > 0) {
78
- target.set(name, bundle);
79
- }
80
- }
81
- } catch {
82
- // Skip invalid bundle files
83
- }
84
- }
85
-
86
- export function loadBundles(cwd: string): Map<string, SkillBundle> {
87
- const bundles = new Map<string, SkillBundle>();
88
- for (const [name, bundle] of Object.entries(DEFAULT_BUNDLES)) {
89
- bundles.set(name, bundle);
90
- }
91
-
92
- const paths = [
93
- join(getAgentDir(), "skill-bundles.json"),
94
- join(getAgentDir(), "skill-bundles.yaml"),
95
- join(getAgentDir(), "skill-bundles.yml"),
96
- join(cwd, ".pi", "skill-bundles.json"),
97
- join(cwd, ".pi", "skill-bundles.yaml"),
98
- join(cwd, ".pi", "skill-bundles.yml"),
99
- ];
100
- for (const path of paths) {
101
- if (existsSync(path)) mergeBundleFile(bundles, path);
102
- }
103
-
104
- return bundles;
105
- }
106
-
107
- export function expandSkillNames(
108
- names: string[],
109
- bundles: Map<string, SkillBundle>,
110
- ): { skills: string[]; modeHint?: SkillBundle["default_mode"] } {
111
- const expanded: string[] = [];
112
- let modeHint: SkillBundle["default_mode"] | undefined;
113
-
114
- for (const name of names) {
115
- const bundleName = name.startsWith("@") ? name.slice(1) : name;
116
- const bundle = bundles.get(bundleName);
117
- if (name.startsWith("@") && bundle) {
118
- expanded.push(...bundle.skills);
119
- modeHint = modeHint ?? bundle.default_mode;
120
- } else {
121
- expanded.push(name);
122
- }
123
- }
124
-
125
- return { skills: [...new Set(expanded)], modeHint };
126
- }
127
-
128
- export function bundleOrderHint(
129
- names: string[],
130
- bundles: Map<string, SkillBundle>,
131
- ): SkillBundle["order"] | undefined {
132
- for (const name of names) {
133
- if (!name.startsWith("@")) continue;
134
- const bundle = bundles.get(name.slice(1));
135
- if (bundle?.order) return bundle.order;
136
- }
137
- return undefined;
138
- }
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { SkillBundle } from "./types.ts";
5
+ import { parseYamlBundles } from "./yaml-bundles.ts";
6
+
7
+ export const DEFAULT_BUNDLES: Record<string, SkillBundle> = {
8
+ "bmad-planning": {
9
+ description: "BMAD Phase 1-2 — analysis & planning",
10
+ skills: ["bmad-master", "analyst", "pm"],
11
+ order: "process-first",
12
+ default_mode: "meta",
13
+ requires: "BMAD Method",
14
+ install:
15
+ "git clone BMAD-METHOD to ~/.claude/skills/bmad, add path in pi settings \"skills\"",
16
+ },
17
+ "bmad-solutioning": {
18
+ description: "BMAD Phase 3 — architecture & design",
19
+ skills: ["bmad-master", "architect", "ux-designer"],
20
+ order: "process-first",
21
+ default_mode: "meta",
22
+ requires: "BMAD Method",
23
+ install:
24
+ "git clone BMAD-METHOD to ~/.claude/skills/bmad, add path in pi settings \"skills\"",
25
+ },
26
+ "bmad-build": {
27
+ description: "BMAD Phase 4 — story implementation",
28
+ skills: ["bmad-master", "developer", "scrum-master"],
29
+ order: "process-first",
30
+ default_mode: "lazy",
31
+ requires: "BMAD Method",
32
+ install:
33
+ "git clone BMAD-METHOD to ~/.claude/skills/bmad, add path in pi settings \"skills\"",
34
+ },
35
+ "cc-feature": {
36
+ description: "Claude Code-style feature workflow",
37
+ skills: [
38
+ "using-superpowers",
39
+ "brainstorming",
40
+ "writing-plans",
41
+ "test-driven-development",
42
+ "requesting-code-review",
43
+ ],
44
+ order: "process-first",
45
+ default_mode: "lazy",
46
+ requires: "Superpowers",
47
+ install:
48
+ "Install Claude Code Superpowers plugin, or copy skills to ~/.pi/agent/skills",
49
+ },
50
+ debug: {
51
+ description: "Systematic bug investigation",
52
+ skills: ["systematic-debugging", "test-driven-development"],
53
+ order: "process-first",
54
+ default_mode: "full",
55
+ requires: "Superpowers",
56
+ install:
57
+ "Install Claude Code Superpowers plugin, or copy skills to ~/.pi/agent/skills",
58
+ },
59
+ };
60
+
61
+ function getAgentDir(): string {
62
+ return join(homedir(), ".pi", "agent");
63
+ }
64
+
65
+ function mergeBundleFile(
66
+ target: Map<string, SkillBundle>,
67
+ path: string,
68
+ ): void {
69
+ try {
70
+ const raw = readFileSync(path, "utf-8");
71
+ const parsed =
72
+ path.endsWith(".yaml") || path.endsWith(".yml")
73
+ ? { bundles: parseYamlBundles(raw) }
74
+ : (JSON.parse(raw) as { bundles?: Record<string, SkillBundle> });
75
+ if (!parsed.bundles) return;
76
+ for (const [name, bundle] of Object.entries(parsed.bundles)) {
77
+ if (Array.isArray(bundle.skills) && bundle.skills.length > 0) {
78
+ target.set(name, bundle);
79
+ }
80
+ }
81
+ } catch {
82
+ // Skip invalid bundle files
83
+ }
84
+ }
85
+
86
+ export function loadBundles(cwd: string): Map<string, SkillBundle> {
87
+ const bundles = new Map<string, SkillBundle>();
88
+ for (const [name, bundle] of Object.entries(DEFAULT_BUNDLES)) {
89
+ bundles.set(name, bundle);
90
+ }
91
+
92
+ const paths = [
93
+ join(getAgentDir(), "skill-bundles.json"),
94
+ join(getAgentDir(), "skill-bundles.yaml"),
95
+ join(getAgentDir(), "skill-bundles.yml"),
96
+ join(cwd, ".pi", "skill-bundles.json"),
97
+ join(cwd, ".pi", "skill-bundles.yaml"),
98
+ join(cwd, ".pi", "skill-bundles.yml"),
99
+ ];
100
+ for (const path of paths) {
101
+ if (existsSync(path)) mergeBundleFile(bundles, path);
102
+ }
103
+
104
+ return bundles;
105
+ }
106
+
107
+ export function expandSkillNames(
108
+ names: string[],
109
+ bundles: Map<string, SkillBundle>,
110
+ ): { skills: string[]; modeHint?: SkillBundle["default_mode"] } {
111
+ const expanded: string[] = [];
112
+ let modeHint: SkillBundle["default_mode"] | undefined;
113
+
114
+ for (const name of names) {
115
+ const bundleName = name.startsWith("@") ? name.slice(1) : name;
116
+ const bundle = bundles.get(bundleName);
117
+ if (name.startsWith("@") && bundle) {
118
+ expanded.push(...bundle.skills);
119
+ modeHint = modeHint ?? bundle.default_mode;
120
+ } else {
121
+ expanded.push(name);
122
+ }
123
+ }
124
+
125
+ return { skills: [...new Set(expanded)], modeHint };
126
+ }
127
+
128
+ export function bundleOrderHint(
129
+ names: string[],
130
+ bundles: Map<string, SkillBundle>,
131
+ ): SkillBundle["order"] | undefined {
132
+ for (const name of names) {
133
+ if (!name.startsWith("@")) continue;
134
+ const bundle = bundles.get(name.slice(1));
135
+ if (bundle?.order) return bundle.order;
136
+ }
137
+ return undefined;
138
+ }
package/src/discover.ts CHANGED
@@ -1,161 +1,161 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import {
5
- getAgentDir,
6
- loadSkills,
7
- loadSkillsFromDir,
8
- type ExtensionAPI,
9
- } from "@earendil-works/pi-coding-agent";
10
- import { truncateDescription } from "./metadata.ts";
11
- import type { SkillInfo } from "./types.ts";
12
-
13
- function readSettingsSkillPaths(): string[] {
14
- try {
15
- const raw = readFileSync(join(getAgentDir(), "settings.json"), "utf-8");
16
- const parsed = JSON.parse(raw) as { skills?: string[] };
17
- return Array.isArray(parsed.skills) ? parsed.skills : [];
18
- } catch {
19
- return [];
20
- }
21
- }
22
-
23
- function fromPiCommands(pi: ExtensionAPI): Map<string, SkillInfo> {
24
- const map = new Map<string, SkillInfo>();
25
- for (const cmd of pi.getCommands()) {
26
- if (cmd.source !== "skill") continue;
27
- const name = cmd.name.startsWith("skill:")
28
- ? cmd.name.slice(6)
29
- : cmd.name;
30
- map.set(name, {
31
- name,
32
- description: truncateDescription(cmd.description || ""),
33
- filePath: cmd.sourceInfo.path,
34
- baseDir: cmd.sourceInfo.baseDir || dirname(cmd.sourceInfo.path),
35
- });
36
- }
37
- return map;
38
- }
39
-
40
- /** Claude Code plugin cache — superpowers and other plugin skills not in pi settings. */
41
- function discoverClaudePluginSkills(): SkillInfo[] {
42
- const cacheRoot = join(homedir(), ".claude", "plugins", "cache");
43
- if (!existsSync(cacheRoot)) return [];
44
-
45
- const byName = new Map<string, SkillInfo>();
46
- try {
47
- for (const pluginEntry of readdirSync(cacheRoot, { withFileTypes: true })) {
48
- if (!pluginEntry.isDirectory()) continue;
49
- const pluginRoot = join(cacheRoot, pluginEntry.name);
50
- for (const versionEntry of readdirSync(pluginRoot, {
51
- withFileTypes: true,
52
- })) {
53
- if (!versionEntry.isDirectory()) continue;
54
- const skillsDir = join(pluginRoot, versionEntry.name, "skills");
55
- if (!existsSync(skillsDir)) continue;
56
- const result = loadSkillsFromDir({
57
- dir: skillsDir,
58
- source: "claude-plugin",
59
- });
60
- for (const skill of result.skills) {
61
- byName.set(skill.name, {
62
- name: skill.name,
63
- description: truncateDescription(skill.description || ""),
64
- filePath: skill.filePath,
65
- baseDir: skill.baseDir,
66
- });
67
- }
68
- }
69
- }
70
- } catch {
71
- // Non-fatal
72
- }
73
- return [...byName.values()];
74
- }
75
-
76
- function discoverCursorSkills(): SkillInfo[] {
77
- const dir = join(homedir(), ".cursor", "skills-cursor");
78
- if (!existsSync(dir)) return [];
79
- try {
80
- return loadSkillsFromDir({ dir, source: "cursor" }).skills.map((skill) => ({
81
- name: skill.name,
82
- description: truncateDescription(skill.description || ""),
83
- filePath: skill.filePath,
84
- baseDir: skill.baseDir,
85
- }));
86
- } catch {
87
- return [];
88
- }
89
- }
90
-
91
- /**
92
- * Discover skills from every source pi and Claude Code use:
93
- * getCommands(), settings skill paths, defaults, plugin cache, cursor skills.
94
- */
95
- export function discoverAllSkills(
96
- pi: ExtensionAPI,
97
- cwd: string,
98
- ): SkillInfo[] {
99
- const map = fromPiCommands(pi);
100
-
101
- const loaded = loadSkills({
102
- cwd,
103
- agentDir: getAgentDir(),
104
- skillPaths: readSettingsSkillPaths(),
105
- includeDefaults: true,
106
- });
107
- for (const skill of loaded.skills) {
108
- if (!map.has(skill.name)) {
109
- map.set(skill.name, {
110
- name: skill.name,
111
- description: truncateDescription(skill.description || ""),
112
- filePath: skill.filePath,
113
- baseDir: skill.baseDir,
114
- });
115
- }
116
- }
117
-
118
- for (const skill of [
119
- ...discoverClaudePluginSkills(),
120
- ...discoverCursorSkills(),
121
- ]) {
122
- if (!map.has(skill.name)) map.set(skill.name, skill);
123
- }
124
-
125
- return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
126
- }
127
-
128
- /** Resolve a skill by name when it was requested but missing from the primary index. */
129
- export function resolveSkillByName(
130
- name: string,
131
- known: Map<string, SkillInfo>,
132
- ): SkillInfo | undefined {
133
- if (known.has(name)) return known.get(name);
134
-
135
- for (const skill of discoverClaudePluginSkills()) {
136
- if (skill.name === name) return skill;
137
- }
138
- for (const skill of discoverCursorSkills()) {
139
- if (skill.name === name) return skill;
140
- }
141
-
142
- const dirs = [
143
- join(getAgentDir(), "skills"),
144
- join(cwd, ".pi", "skills"),
145
- ];
146
- for (const dir of dirs) {
147
- for (const candidate of [
148
- join(dir, name, "SKILL.md"),
149
- join(dir, `${name}.md`),
150
- ]) {
151
- if (!existsSync(candidate)) continue;
152
- return {
153
- name,
154
- description: "",
155
- filePath: candidate,
156
- baseDir: dirname(candidate),
157
- };
158
- }
159
- }
160
- return undefined;
161
- }
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import {
5
+ getAgentDir,
6
+ loadSkills,
7
+ loadSkillsFromDir,
8
+ type ExtensionAPI,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { truncateDescription } from "./metadata.ts";
11
+ import type { SkillInfo } from "./types.ts";
12
+
13
+ function readSettingsSkillPaths(): string[] {
14
+ try {
15
+ const raw = readFileSync(join(getAgentDir(), "settings.json"), "utf-8");
16
+ const parsed = JSON.parse(raw) as { skills?: string[] };
17
+ return Array.isArray(parsed.skills) ? parsed.skills : [];
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ function fromPiCommands(pi: ExtensionAPI): Map<string, SkillInfo> {
24
+ const map = new Map<string, SkillInfo>();
25
+ for (const cmd of pi.getCommands()) {
26
+ if (cmd.source !== "skill") continue;
27
+ const name = cmd.name.startsWith("skill:")
28
+ ? cmd.name.slice(6)
29
+ : cmd.name;
30
+ map.set(name, {
31
+ name,
32
+ description: truncateDescription(cmd.description || ""),
33
+ filePath: cmd.sourceInfo.path,
34
+ baseDir: cmd.sourceInfo.baseDir || dirname(cmd.sourceInfo.path),
35
+ });
36
+ }
37
+ return map;
38
+ }
39
+
40
+ /** Claude Code plugin cache — superpowers and other plugin skills not in pi settings. */
41
+ function discoverClaudePluginSkills(): SkillInfo[] {
42
+ const cacheRoot = join(homedir(), ".claude", "plugins", "cache");
43
+ if (!existsSync(cacheRoot)) return [];
44
+
45
+ const byName = new Map<string, SkillInfo>();
46
+ try {
47
+ for (const pluginEntry of readdirSync(cacheRoot, { withFileTypes: true })) {
48
+ if (!pluginEntry.isDirectory()) continue;
49
+ const pluginRoot = join(cacheRoot, pluginEntry.name);
50
+ for (const versionEntry of readdirSync(pluginRoot, {
51
+ withFileTypes: true,
52
+ })) {
53
+ if (!versionEntry.isDirectory()) continue;
54
+ const skillsDir = join(pluginRoot, versionEntry.name, "skills");
55
+ if (!existsSync(skillsDir)) continue;
56
+ const result = loadSkillsFromDir({
57
+ dir: skillsDir,
58
+ source: "claude-plugin",
59
+ });
60
+ for (const skill of result.skills) {
61
+ byName.set(skill.name, {
62
+ name: skill.name,
63
+ description: truncateDescription(skill.description || ""),
64
+ filePath: skill.filePath,
65
+ baseDir: skill.baseDir,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ } catch {
71
+ // Non-fatal
72
+ }
73
+ return [...byName.values()];
74
+ }
75
+
76
+ function discoverCursorSkills(): SkillInfo[] {
77
+ const dir = join(homedir(), ".cursor", "skills-cursor");
78
+ if (!existsSync(dir)) return [];
79
+ try {
80
+ return loadSkillsFromDir({ dir, source: "cursor" }).skills.map((skill) => ({
81
+ name: skill.name,
82
+ description: truncateDescription(skill.description || ""),
83
+ filePath: skill.filePath,
84
+ baseDir: skill.baseDir,
85
+ }));
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Discover skills from every source pi and Claude Code use:
93
+ * getCommands(), settings skill paths, defaults, plugin cache, cursor skills.
94
+ */
95
+ export function discoverAllSkills(
96
+ pi: ExtensionAPI,
97
+ cwd: string,
98
+ ): SkillInfo[] {
99
+ const map = fromPiCommands(pi);
100
+
101
+ const loaded = loadSkills({
102
+ cwd,
103
+ agentDir: getAgentDir(),
104
+ skillPaths: readSettingsSkillPaths(),
105
+ includeDefaults: true,
106
+ });
107
+ for (const skill of loaded.skills) {
108
+ if (!map.has(skill.name)) {
109
+ map.set(skill.name, {
110
+ name: skill.name,
111
+ description: truncateDescription(skill.description || ""),
112
+ filePath: skill.filePath,
113
+ baseDir: skill.baseDir,
114
+ });
115
+ }
116
+ }
117
+
118
+ for (const skill of [
119
+ ...discoverClaudePluginSkills(),
120
+ ...discoverCursorSkills(),
121
+ ]) {
122
+ if (!map.has(skill.name)) map.set(skill.name, skill);
123
+ }
124
+
125
+ return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
126
+ }
127
+
128
+ /** Resolve a skill by name when it was requested but missing from the primary index. */
129
+ export function resolveSkillByName(
130
+ name: string,
131
+ known: Map<string, SkillInfo>,
132
+ ): SkillInfo | undefined {
133
+ if (known.has(name)) return known.get(name);
134
+
135
+ for (const skill of discoverClaudePluginSkills()) {
136
+ if (skill.name === name) return skill;
137
+ }
138
+ for (const skill of discoverCursorSkills()) {
139
+ if (skill.name === name) return skill;
140
+ }
141
+
142
+ const dirs = [
143
+ join(getAgentDir(), "skills"),
144
+ join(cwd, ".pi", "skills"),
145
+ ];
146
+ for (const dir of dirs) {
147
+ for (const candidate of [
148
+ join(dir, name, "SKILL.md"),
149
+ join(dir, `${name}.md`),
150
+ ]) {
151
+ if (!existsSync(candidate)) continue;
152
+ return {
153
+ name,
154
+ description: "",
155
+ filePath: candidate,
156
+ baseDir: dirname(candidate),
157
+ };
158
+ }
159
+ }
160
+ return undefined;
161
+ }
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  } from "./bundle-status.ts";
19
19
  import { bmadAutoHint, resolveBmadAutoSkills } from "./bmad-auto.ts";
20
20
  import { buildBmadStatusBlock, shouldInjectBmadStatus } from "./bmad-status.ts";
21
- import { buildCombinedMessage, resolveAndReadLegacySkill } from "./build.ts";
21
+ import { buildCombinedMessage } from "./build.ts";
22
22
  import { getSkillsArgumentCompletions } from "./completions.ts";
23
23
  import { resolveSkillConflicts } from "./conflicts.ts";
24
24
  import { discoverAllSkills, resolveSkillByName } from "./discover.ts";
@@ -347,22 +347,18 @@ export default function (pi: ExtensionAPI) {
347
347
  skillNames,
348
348
  available,
349
349
  );
350
- const blocks = selected
351
- .map((s) => resolveAndReadLegacySkill(s.name, s.filePath, ctx.cwd))
352
- .filter((b): b is string => b !== null);
353
-
354
- if (blocks.length === 0) return { action: "continue" };
355
-
356
- const wrapped = [
357
- `<manually_attached_skills count="${blocks.length}">`,
358
- ...blocks,
359
- args ? `\n<user_query>\n${args}\n</user_query>` : "",
360
- "</manually_attached_skills>",
361
- ]
362
- .filter(Boolean)
363
- .join("\n\n");
364
-
365
- return { action: "transform", text: wrapped };
350
+
351
+ if (selected.length === 0) return { action: "continue" };
352
+
353
+ const { message } = buildCombinedMessage(selected, ctx.cwd, {
354
+ mode: "full",
355
+ parallel: false,
356
+ instructions: args || undefined,
357
+ });
358
+
359
+ if (!message.includes("<skill ")) return { action: "continue" };
360
+
361
+ return { action: "transform", text: message };
366
362
  });
367
363
  }
368
364