@miphamai/cli 0.54.0 → 0.55.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/package.json +1 -1
- package/src/agent/agent-registry.ts +5 -2
- package/src/agent/cross-session/discovery.ts +3 -1
- package/src/agent/message-router.ts +10 -0
- package/src/commands/project.ts +4 -3
- package/src/config/loader.ts +2 -2
- package/src/core/instructions.ts +15 -2
- package/src/index.tsx +45 -32
- package/src/shared/atomic-write.ts +17 -0
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +1 -0
- package/src/skills/loader.ts +5 -2
- package/src/ui/config-wizard.tsx +3 -2
- package/src/core/output-styles.ts +0 -95
package/package.json
CHANGED
|
@@ -10,9 +10,12 @@ interface FrontmatterResult {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function parseFrontmatter(raw: string): FrontmatterResult {
|
|
13
|
-
|
|
13
|
+
// Strip a leading UTF-8 BOM — otherwise `^---` never matches and a
|
|
14
|
+
// BOM-prefixed file is silently treated as body text (effectively ignored).
|
|
15
|
+
const src = raw.replace(/^\uFEFF/, '')
|
|
16
|
+
const match = src.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
|
|
14
17
|
if (!match) {
|
|
15
|
-
return { data: {}, content:
|
|
18
|
+
return { data: {}, content: src }
|
|
16
19
|
}
|
|
17
20
|
return {
|
|
18
21
|
data: parseYaml(match[1] || '') as Record<string, unknown>,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from 'node:fs'
|
|
10
10
|
import { join, basename } from 'node:path'
|
|
11
11
|
import { homedir, hostname } from 'node:os'
|
|
12
|
-
import type { SessionInfo } from '../../shared/types'
|
|
12
|
+
import type { SessionInfo, CrossSessionInbound } from '../../shared/types'
|
|
13
13
|
|
|
14
14
|
const MIPHAM_HOME = join(homedir(), '.mipham')
|
|
15
15
|
const ACTIVE_SESSIONS_DIR = join(MIPHAM_HOME, '.active-sessions')
|
|
@@ -97,6 +97,7 @@ export function createSessionInfo(
|
|
|
97
97
|
cwd?: string,
|
|
98
98
|
provider?: string,
|
|
99
99
|
model?: string,
|
|
100
|
+
crossSessionInbound?: CrossSessionInbound,
|
|
100
101
|
): SessionInfo {
|
|
101
102
|
return {
|
|
102
103
|
id: sessionId,
|
|
@@ -107,6 +108,7 @@ export function createSessionInfo(
|
|
|
107
108
|
cwd,
|
|
108
109
|
provider,
|
|
109
110
|
model,
|
|
111
|
+
crossSessionInbound,
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -90,6 +90,16 @@ export class MessageRouter {
|
|
|
90
90
|
}
|
|
91
91
|
const targetSession = resolution.session!
|
|
92
92
|
|
|
93
|
+
// Recipient refuses inbound messages — report "refused" instead of a silent
|
|
94
|
+
// success so the sender knows the message was not delivered.
|
|
95
|
+
if (targetSession.crossSessionInbound === 'deny') {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
routedTo: 'inbox',
|
|
99
|
+
error: `Session "${targetSession.name}" refuses inbound messages.`,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
// Get sender info — resolve the human-readable name when the sender is a
|
|
94
104
|
// registered session, else fall back to the raw id (e.g. non-session senders).
|
|
95
105
|
const senderName = discoverSessions().find((s) => s.id === from)?.name ?? from
|
package/src/commands/project.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { CommandHandler, CommandContext, CommandResult } from '../ui/commands.js'
|
|
9
9
|
import { getWorkspaceTrust } from '../core/workspace-trust'
|
|
10
|
+
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
10
11
|
|
|
11
12
|
export {
|
|
12
13
|
initCmd,
|
|
@@ -20,7 +21,7 @@ export {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const initCmd: CommandHandler = async (ctx) => {
|
|
23
|
-
const { existsSync, mkdirSync
|
|
24
|
+
const { existsSync, mkdirSync } = await import('node:fs')
|
|
24
25
|
const { join } = await import('node:path')
|
|
25
26
|
const { homedir } = await import('node:os')
|
|
26
27
|
|
|
@@ -78,7 +79,7 @@ providers:
|
|
|
78
79
|
${providerYaml}
|
|
79
80
|
`
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
atomicWriteFileSync(userConfigPath, configContent)
|
|
82
83
|
return {
|
|
83
84
|
content: `✅ Mipham Code initialized!
|
|
84
85
|
|
|
@@ -434,7 +435,7 @@ permission: ask
|
|
|
434
435
|
providers:
|
|
435
436
|
${providerYaml}
|
|
436
437
|
`
|
|
437
|
-
|
|
438
|
+
atomicWriteFileSync(configPath, defaultConfig)
|
|
438
439
|
created.push('.mipham/config.yml')
|
|
439
440
|
} else {
|
|
440
441
|
skipped.push('.mipham/config.yml (already exists)')
|
package/src/config/loader.ts
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
readdirSync,
|
|
7
7
|
unlinkSync,
|
|
8
8
|
chmodSync,
|
|
9
|
-
writeFileSync,
|
|
10
9
|
} from 'node:fs'
|
|
11
10
|
import { join } from 'node:path'
|
|
12
11
|
import { homedir } from 'node:os'
|
|
13
12
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
13
|
+
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
14
14
|
import type {
|
|
15
15
|
MiphamConfig,
|
|
16
16
|
ProviderConfig,
|
|
@@ -533,7 +533,7 @@ export function saveProviderApiKey(providerId: string, apiKey: string): boolean
|
|
|
533
533
|
doc.providers = providers
|
|
534
534
|
|
|
535
535
|
// Write back
|
|
536
|
-
|
|
536
|
+
atomicWriteFileSync(configPath, stringifyYaml(doc), { mode: 0o600 })
|
|
537
537
|
return true
|
|
538
538
|
} catch (err: unknown) {
|
|
539
539
|
const msg = err instanceof Error ? err.message : String(err)
|
package/src/core/instructions.ts
CHANGED
|
@@ -10,9 +10,12 @@ interface FrontmatterResult {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function parseFrontmatter(raw: string): FrontmatterResult {
|
|
13
|
-
|
|
13
|
+
// Strip a leading UTF-8 BOM — otherwise `^---` never matches and a
|
|
14
|
+
// BOM-prefixed file is silently treated as body text (effectively ignored).
|
|
15
|
+
const src = raw.replace(/^\uFEFF/, '')
|
|
16
|
+
const match = src.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
14
17
|
if (!match) {
|
|
15
|
-
return { data: {}, content:
|
|
18
|
+
return { data: {}, content: src }
|
|
16
19
|
}
|
|
17
20
|
return {
|
|
18
21
|
data: parseYaml(match[1] || '') as Record<string, unknown>,
|
|
@@ -101,6 +104,16 @@ export class InstructionsLoader {
|
|
|
101
104
|
parts.push(this.buildPermissionContext(permissionMode))
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
// 开场克制:寒暄只回一句短问候,不上能力清单(避免把「你好」当「你是谁」处理)
|
|
108
|
+
parts.push(`## Greeting Restraint
|
|
109
|
+
|
|
110
|
+
When the user's message is only a greeting or small talk ("hello", "hi",
|
|
111
|
+
"你好", "您好", "在吗", "早上好"), reply with ONE brief, warm line —
|
|
112
|
+
e.g. "你好,我是 Mipham,有什么可以帮你的?" Do NOT introduce yourself,
|
|
113
|
+
list your capabilities/tools/MCP servers, or enumerate what you can do.
|
|
114
|
+
Only describe your capabilities when the user explicitly asks ("你是谁",
|
|
115
|
+
"你能做什么", "what can you do", "introduce yourself").`)
|
|
116
|
+
|
|
104
117
|
// Inject critical thinking self-check layer (for analysis/comparison tasks)
|
|
105
118
|
parts.push(`## Critical Thinking Self-Check
|
|
106
119
|
|
package/src/index.tsx
CHANGED
|
@@ -28,7 +28,7 @@ import { QueryEngine } from './core/engine'
|
|
|
28
28
|
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
29
29
|
import { SessionLog } from './core/session-log'
|
|
30
30
|
import { SessionStore } from './core/session-store'
|
|
31
|
-
import type { PermissionLevel, MiphamConfig } from './shared/types'
|
|
31
|
+
import type { PermissionLevel, MiphamConfig, McpServerConfig } from './shared/types'
|
|
32
32
|
import { SkillsLoader } from './skills/loader'
|
|
33
33
|
import { PluginManager } from './plugin/plugin-manager'
|
|
34
34
|
import { loadPlugins } from './plugin/plugin-loader'
|
|
@@ -194,6 +194,38 @@ function SetupGate(props: SetupGateProps) {
|
|
|
194
194
|
})
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Connect all MCP servers in parallel and register their tools into the shared
|
|
199
|
+
* registry. Runs lazily in the background so a slow or dead server (e.g. a 15s
|
|
200
|
+
* connect timeout) never blocks first paint. Failures are non-fatal.
|
|
201
|
+
*/
|
|
202
|
+
async function connectMcpServers(
|
|
203
|
+
mcpServers: McpServerConfig[],
|
|
204
|
+
tools: ReturnType<typeof createToolRegistry>,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
if (mcpServers.length === 0) return
|
|
207
|
+
const mcp = McpClient.getInstance()
|
|
208
|
+
// Wire runtime tool-list changes before connect so mid-connect updates land.
|
|
209
|
+
syncMcpToolsOnChange(mcp, tools)
|
|
210
|
+
const results = await Promise.allSettled(
|
|
211
|
+
mcpServers.map(async (server) => {
|
|
212
|
+
await mcp.connect(server)
|
|
213
|
+
const count = registerMcpServerTools(server.name, tools)
|
|
214
|
+
if (count > 0) {
|
|
215
|
+
process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
for (let i = 0; i < results.length; i++) {
|
|
220
|
+
const result = results[i]!
|
|
221
|
+
if (result.status === 'rejected') {
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`[mcp] Failed to connect "${mcpServers[i]!.name}": ${String(result.reason)}\n`,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
export async function runApp(options: RunOptions): Promise<void> {
|
|
198
230
|
// Metrics: count CLI invocation + active session
|
|
199
231
|
getMetrics().cliInvocations.inc()
|
|
@@ -455,33 +487,10 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
455
487
|
mountSkills(vajraContext, skillsLoader)
|
|
456
488
|
mountLlm(vajraContext, registry)
|
|
457
489
|
|
|
458
|
-
// Connect MCP servers
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const mcp = McpClient.getInstance()
|
|
463
|
-
const results = await Promise.allSettled(
|
|
464
|
-
mcpServers.map(async (server) => {
|
|
465
|
-
await mcp.connect(server)
|
|
466
|
-
const count = registerMcpServerTools(server.name, tools)
|
|
467
|
-
if (count > 0) {
|
|
468
|
-
process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
|
|
469
|
-
}
|
|
470
|
-
}),
|
|
471
|
-
)
|
|
472
|
-
// Log failures (non-fatal — app starts without that server's tools)
|
|
473
|
-
for (let i = 0; i < results.length; i++) {
|
|
474
|
-
const result = results[i]!
|
|
475
|
-
if (result.status === 'rejected') {
|
|
476
|
-
process.stderr.write(
|
|
477
|
-
`[mcp] Failed to connect "${mcpServers[i]!.name}": ${String(result.reason)}\n`,
|
|
478
|
-
)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Wire runtime MCP tool changes (tools/list_changed) into the central registry.
|
|
484
|
-
syncMcpToolsOnChange(McpClient.getInstance(), tools)
|
|
490
|
+
// Connect MCP servers lazily in the background: the UI renders immediately,
|
|
491
|
+
// and each server's tools register into the shared registry as it connects.
|
|
492
|
+
// A dead server (e.g. a 15s connect timeout) no longer blocks startup.
|
|
493
|
+
void connectMcpServers(config.skills?.mcpServers ?? [], tools)
|
|
485
494
|
|
|
486
495
|
// Initialize hook engine — register skill-defined hooks
|
|
487
496
|
const hookEngine = new HookEngine()
|
|
@@ -549,12 +558,20 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
549
558
|
// so `@mipham-code` can address this session; uniqueness is enforced against
|
|
550
559
|
// other live sessions (suffix -2, -3, …). The id stays a stable `session-<ts>`.
|
|
551
560
|
const defaultName = basename(process.cwd()) || 'session'
|
|
561
|
+
|
|
562
|
+
// Load cross-session config first so the registered SessionInfo carries this
|
|
563
|
+
// session's inbound policy — senders discover it and report "refused" instead
|
|
564
|
+
// of a silent drop when policy is 'deny'.
|
|
565
|
+
const crossSessionConfig = loadCrossSessionConfig(process.cwd())
|
|
566
|
+
engine.setCrossSessionConfig(crossSessionConfig)
|
|
567
|
+
|
|
552
568
|
const sessionInfo = createSessionInfo(
|
|
553
569
|
sessionName,
|
|
554
570
|
ensureUniqueSessionName(defaultName, discoverSessions()),
|
|
555
571
|
process.cwd(),
|
|
556
572
|
defaultProvider,
|
|
557
573
|
defaultModel,
|
|
574
|
+
crossSessionConfig.crossSessionInbound,
|
|
558
575
|
)
|
|
559
576
|
registerActiveSession(sessionInfo)
|
|
560
577
|
|
|
@@ -568,10 +585,6 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
568
585
|
heartbeatInterval.unref()
|
|
569
586
|
}
|
|
570
587
|
|
|
571
|
-
// Load cross-session config and wire into the engine's inbound policy
|
|
572
|
-
const crossSessionConfig = loadCrossSessionConfig(process.cwd())
|
|
573
|
-
engine.setCrossSessionConfig(crossSessionConfig)
|
|
574
|
-
|
|
575
588
|
// P2-1: Trigger SessionStart hooks after full initialization
|
|
576
589
|
hookEngine.executeSessionStart(sessionName).catch(() => {
|
|
577
590
|
// Hook failures never block session startup
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { writeFileSync, renameSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write a file atomically: write to a same-directory `.tmp` file, then rename
|
|
5
|
+
* over the target. Same-filesystem rename is atomic, so a crash or kill
|
|
6
|
+
* mid-write can never leave a truncated/corrupt file — readers see either the
|
|
7
|
+
* old or the new content, never a partial write.
|
|
8
|
+
*/
|
|
9
|
+
export function atomicWriteFileSync(
|
|
10
|
+
path: string,
|
|
11
|
+
content: string,
|
|
12
|
+
options: { mode?: number } = {},
|
|
13
|
+
): void {
|
|
14
|
+
const tmp = path + '.tmp'
|
|
15
|
+
writeFileSync(tmp, content, { encoding: 'utf-8', mode: options.mode ?? 0o600 })
|
|
16
|
+
renameSync(tmp, path)
|
|
17
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.55.0' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
package/src/shared/types.ts
CHANGED
package/src/skills/loader.ts
CHANGED
|
@@ -13,9 +13,12 @@ interface FrontmatterResult {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function parseFrontmatter(raw: string): FrontmatterResult {
|
|
16
|
-
|
|
16
|
+
// Strip a leading UTF-8 BOM — otherwise `^---` never matches and a
|
|
17
|
+
// BOM-prefixed file is silently treated as body text (effectively ignored).
|
|
18
|
+
const src = raw.replace(/^\uFEFF/, '')
|
|
19
|
+
const match = src.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
17
20
|
if (!match) {
|
|
18
|
-
return { data: {}, content:
|
|
21
|
+
return { data: {}, content: src }
|
|
19
22
|
}
|
|
20
23
|
return {
|
|
21
24
|
data: parseYaml(match[1] || '') as Record<string, unknown>,
|
package/src/ui/config-wizard.tsx
CHANGED
|
@@ -14,7 +14,8 @@ import { useI18n } from '../i18n-context'
|
|
|
14
14
|
import TextInput from 'ink-text-input'
|
|
15
15
|
import { DEFAULT_PROVIDERS, OLLAMA_PRESET_MODELS } from '../shared/constants'
|
|
16
16
|
import type { ModelInfo } from '../shared/types'
|
|
17
|
-
import { mkdirSync
|
|
17
|
+
import { mkdirSync } from 'node:fs'
|
|
18
|
+
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
18
19
|
import { join } from 'node:path'
|
|
19
20
|
import { homedir } from 'node:os'
|
|
20
21
|
import { execSync } from 'node:child_process'
|
|
@@ -115,7 +116,7 @@ function writeConfigFile(providerId: string, modelId: string, apiKey: string): v
|
|
|
115
116
|
...models.map((m) => ` - id: ${m.id}`),
|
|
116
117
|
]
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
atomicWriteFileSync(join(configDir, 'config.yml'), lines.join('\n'))
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OutputStylesLoader — loads custom response persona styles from .mipham/output-styles/.
|
|
3
|
-
*
|
|
4
|
-
* Each .md file in the directory is a named style. The active style's content
|
|
5
|
-
* is injected into the system prompt to shape the AI's tone and communication style.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const loader = new OutputStylesLoader(cwd)
|
|
9
|
-
* loader.list() // ['concise', 'academic', ...]
|
|
10
|
-
* loader.get('concise') // style content or undefined
|
|
11
|
-
* loader.setActive('concise') // switch active style
|
|
12
|
-
* loader.getActiveContent() // active style content (for system prompt)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
|
16
|
-
import { join, basename, extname } from 'node:path'
|
|
17
|
-
|
|
18
|
-
export class OutputStylesLoader {
|
|
19
|
-
private stylesDir: string
|
|
20
|
-
private activeStyle: string | null = null
|
|
21
|
-
|
|
22
|
-
constructor(cwd: string) {
|
|
23
|
-
this.stylesDir = join(cwd, '.mipham', 'output-styles')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* List all available style names (filename without .md extension).
|
|
28
|
-
*/
|
|
29
|
-
list(): string[] {
|
|
30
|
-
if (!existsSync(this.stylesDir)) return []
|
|
31
|
-
try {
|
|
32
|
-
return readdirSync(this.stylesDir)
|
|
33
|
-
.filter((f) => f.endsWith('.md'))
|
|
34
|
-
.map((f) => basename(f, extname(f)))
|
|
35
|
-
} catch {
|
|
36
|
-
return []
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get the content of a named style. Returns undefined if not found.
|
|
42
|
-
*/
|
|
43
|
-
get(name: string): string | undefined {
|
|
44
|
-
if (!existsSync(this.stylesDir)) return undefined
|
|
45
|
-
const filepath = join(this.stylesDir, `${name}.md`)
|
|
46
|
-
if (!existsSync(filepath)) return undefined
|
|
47
|
-
try {
|
|
48
|
-
return readFileSync(filepath, 'utf-8').trim()
|
|
49
|
-
} catch {
|
|
50
|
-
return undefined
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Set the active output style.
|
|
56
|
-
*/
|
|
57
|
-
setActive(name: string): boolean {
|
|
58
|
-
const content = this.get(name)
|
|
59
|
-
if (!content) return false
|
|
60
|
-
this.activeStyle = name
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Clear the active output style (revert to default personality).
|
|
66
|
-
*/
|
|
67
|
-
clearActive(): void {
|
|
68
|
-
this.activeStyle = null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get the active style name (null if none selected).
|
|
73
|
-
*/
|
|
74
|
-
getActiveName(): string | null {
|
|
75
|
-
return this.activeStyle
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get the active style content for system prompt injection.
|
|
80
|
-
* Returns empty string if no style is active.
|
|
81
|
-
*/
|
|
82
|
-
getActiveContent(): string {
|
|
83
|
-
if (!this.activeStyle) return ''
|
|
84
|
-
const content = this.get(this.activeStyle)
|
|
85
|
-
if (!content) return ''
|
|
86
|
-
return [
|
|
87
|
-
`[Output Style: ${this.activeStyle}]`,
|
|
88
|
-
'Adopt the following communication style in all your responses:',
|
|
89
|
-
'',
|
|
90
|
-
content,
|
|
91
|
-
'',
|
|
92
|
-
'---',
|
|
93
|
-
].join('\n')
|
|
94
|
-
}
|
|
95
|
-
}
|