wispy-cli 2.7.10 → 2.7.12

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/wispy.mjs CHANGED
@@ -23,6 +23,47 @@ const rootDir = join(__dirname, "..");
23
23
  const args = process.argv.slice(2);
24
24
  const command = args[0];
25
25
 
26
+ // ── Global flags parsed early ─────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Extract a flag value: --flag value or --flag=value
30
+ * Returns the value string or null if not found.
31
+ */
32
+ function extractFlag(flagNames, removeFromArgs = false) {
33
+ for (const flag of Array.isArray(flagNames) ? flagNames : [flagNames]) {
34
+ const idx = args.indexOf(flag);
35
+ if (idx !== -1 && idx + 1 < args.length) {
36
+ const value = args[idx + 1];
37
+ if (removeFromArgs) { args.splice(idx, 2); }
38
+ return value;
39
+ }
40
+ // --flag=value form
41
+ const prefix = flag + "=";
42
+ const eqIdx = args.findIndex(a => a.startsWith(prefix));
43
+ if (eqIdx !== -1) {
44
+ const value = args[eqIdx].slice(prefix.length);
45
+ if (removeFromArgs) { args.splice(eqIdx, 1); }
46
+ return value;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** Returns true if a boolean flag is present. */
53
+ function hasFlag(flag) {
54
+ return args.includes(flag);
55
+ }
56
+
57
+ // Parse global flags (order doesn't matter, remove from args so REPL doesn't see them)
58
+ const globalProfile = extractFlag(["--profile", "-p"], true);
59
+ const globalPersonality = extractFlag(["--personality"], true);
60
+ const globalJsonMode = hasFlag("--json");
61
+ if (globalJsonMode) { args.splice(args.indexOf("--json"), 1); }
62
+
63
+ // Expose for submodules via env
64
+ if (globalProfile) process.env.WISPY_PROFILE = globalProfile;
65
+ if (globalPersonality) process.env.WISPY_PERSONALITY = globalPersonality;
66
+
26
67
  // ── Flags ─────────────────────────────────────────────────────────────────────
27
68
 
28
69
  if (args.includes("--version") || command === "--version" || command === "-v") {
@@ -44,13 +85,19 @@ AI workspace assistant — chat, automate, and orchestrate from the terminal.
44
85
  Usage:
45
86
  wispy Start interactive REPL
46
87
  wispy <message> One-shot chat
88
+ wispy exec <message> Non-interactive one-shot (CI/pipeline friendly)
47
89
  wispy setup Run first-time setup wizard
48
90
  wispy config [show|get|set|delete|reset|path|edit]
49
91
  Manage configuration
92
+ wispy features [list|enable|disable]
93
+ Manage feature flags
50
94
  wispy model Show or change AI model
51
95
  wispy doctor Check system health
52
96
  wispy browser [tabs|attach|navigate|screenshot|doctor]
53
97
  Browser control via local-browser-bridge
98
+ wispy secrets [list|set|delete|get]
99
+ Manage encrypted secrets & API keys
100
+ wispy tts "<text>" Text-to-speech (OpenAI or macOS say)
54
101
  wispy trust [level|log] Security level & audit
55
102
  wispy ws [start-client|run-debug]
56
103
  WebSocket operations
@@ -66,9 +113,12 @@ Usage:
66
113
 
67
114
  Options:
68
115
  -w, --workstream <name> Set active workstream
116
+ -p, --profile <name> Use a named config profile
69
117
  --session <id> Resume a session
70
118
  --model <name> Override AI model
71
119
  --provider <name> Override AI provider
120
+ --personality <name> Set personality (pragmatic|concise|explanatory|friendly|strict)
121
+ --json Output JSONL events (for exec command, CI/pipeline use)
72
122
  --help, -h Show this help
73
123
  --version, -v Show version
74
124
  `);
@@ -338,6 +388,133 @@ if (command === "browser") {
338
388
  process.exit(0);
339
389
  }
340
390
 
391
+ // ── Secrets ───────────────────────────────────────────────────────────────────
392
+
393
+ if (command === "secrets") {
394
+ try {
395
+ const { SecretsManager } = await import(join(rootDir, "core/secrets.mjs"));
396
+ const sm = new SecretsManager();
397
+ const sub = args[1];
398
+
399
+ if (!sub || sub === "list") {
400
+ const keys = await sm.list();
401
+ if (keys.length === 0) {
402
+ console.log("No secrets stored. Use: wispy secrets set <key> <value>");
403
+ } else {
404
+ console.log(`\n Stored secrets (${keys.length}):\n`);
405
+ for (const k of keys) {
406
+ console.log(` ${k}`);
407
+ }
408
+ console.log("");
409
+ }
410
+ } else if (sub === "set") {
411
+ const key = args[2];
412
+ const value = args[3];
413
+ if (!key || value === undefined) {
414
+ console.error("Usage: wispy secrets set <key> <value>");
415
+ process.exit(1);
416
+ }
417
+ await sm.set(key, value);
418
+ console.log(`Secret '${key}' stored (encrypted).`);
419
+ } else if (sub === "delete") {
420
+ const key = args[2];
421
+ if (!key) {
422
+ console.error("Usage: wispy secrets delete <key>");
423
+ process.exit(1);
424
+ }
425
+ const result = await sm.delete(key);
426
+ if (result.success) {
427
+ console.log(`Secret '${key}' deleted.`);
428
+ } else {
429
+ console.error(`Secret '${key}' not found.`);
430
+ process.exit(1);
431
+ }
432
+ } else if (sub === "get") {
433
+ const key = args[2];
434
+ if (!key) {
435
+ console.error("Usage: wispy secrets get <key>");
436
+ process.exit(1);
437
+ }
438
+ const value = await sm.resolve(key);
439
+ if (value) {
440
+ console.log(`${key} = ***`);
441
+ console.log("(Use --reveal flag to show value — not recommended)");
442
+ if (args.includes("--reveal")) {
443
+ console.log(`Value: ${value}`);
444
+ }
445
+ } else {
446
+ console.log(`Secret '${key}' not found.`);
447
+ }
448
+ } else {
449
+ console.error(`Unknown secrets subcommand: ${sub}`);
450
+ console.log("Available: list, set <key> <value>, delete <key>, get <key>");
451
+ process.exit(1);
452
+ }
453
+ } catch (err) {
454
+ console.error("Secrets error:", err.message);
455
+ process.exit(1);
456
+ }
457
+ process.exit(0);
458
+ }
459
+
460
+ // ── TTS ───────────────────────────────────────────────────────────────────────
461
+
462
+ if (command === "tts") {
463
+ try {
464
+ const { SecretsManager } = await import(join(rootDir, "core/secrets.mjs"));
465
+ const { TTSManager } = await import(join(rootDir, "core/tts.mjs"));
466
+
467
+ const sm = new SecretsManager();
468
+ const tts = new TTSManager(sm);
469
+
470
+ // Parse args: wispy tts "text" [--voice name] [--provider openai|macos] [--play]
471
+ let textArg = args[1];
472
+ if (!textArg) {
473
+ console.error('Usage: wispy tts "text to speak" [--voice <name>] [--provider openai|macos] [--play]');
474
+ process.exit(1);
475
+ }
476
+
477
+ const voiceIdx = args.indexOf("--voice");
478
+ const providerIdx = args.indexOf("--provider");
479
+ const shouldPlay = args.includes("--play");
480
+
481
+ const voice = voiceIdx !== -1 ? args[voiceIdx + 1] : undefined;
482
+ const provider = providerIdx !== -1 ? args[providerIdx + 1] : "auto";
483
+
484
+ console.log(` Generating speech (provider: ${provider})...`);
485
+ const result = await tts.speak(textArg, { voice, provider });
486
+
487
+ if (result.error) {
488
+ console.error(` TTS Error: ${result.error}`);
489
+ process.exit(1);
490
+ }
491
+
492
+ console.log(` Provider: ${result.provider}`);
493
+ console.log(` Voice: ${result.voice}`);
494
+ console.log(` Format: ${result.format}`);
495
+ console.log(` File: ${result.path}`);
496
+
497
+ if (shouldPlay) {
498
+ const { execFile } = await import("node:child_process");
499
+ const { promisify } = await import("node:util");
500
+ const exec = promisify(execFile);
501
+ try {
502
+ if (process.platform === "darwin") {
503
+ await exec("afplay", [result.path]);
504
+ } else {
505
+ console.log(' Use --play flag only on macOS. Play manually with your audio player.');
506
+ }
507
+ } catch (playErr) {
508
+ console.error(` Playback failed: ${playErr.message}`);
509
+ }
510
+ }
511
+ } catch (err) {
512
+ console.error("TTS error:", err.message);
513
+ process.exit(1);
514
+ }
515
+ process.exit(0);
516
+ }
517
+
341
518
  // ── Trust ─────────────────────────────────────────────────────────────────────
342
519
 
343
520
  if (command === "trust") {
@@ -399,6 +576,131 @@ if (command === "improve") {
399
576
  process.exit(0);
400
577
  }
401
578
 
579
+ // ── Features ──────────────────────────────────────────────────────────────────
580
+
581
+ if (command === "features") {
582
+ try {
583
+ const { FeatureManager } = await import(join(rootDir, "core/features.mjs"));
584
+ const fm = new FeatureManager();
585
+ const sub = args[1];
586
+
587
+ if (!sub || sub === "list") {
588
+ const features = await fm.list();
589
+ const stageOrder = { stable: 0, experimental: 1, development: 2 };
590
+ features.sort((a, b) => (stageOrder[a.stage] ?? 9) - (stageOrder[b.stage] ?? 9) || a.name.localeCompare(b.name));
591
+
592
+ const stageLabel = { stable: "stable", experimental: "experimental", development: "development" };
593
+ const stageColors = { stable: "\x1b[32m", experimental: "\x1b[33m", development: "\x1b[31m" };
594
+ const reset = "\x1b[0m";
595
+ const dim = "\x1b[2m";
596
+
597
+ console.log(`\n Feature Flags (${features.length})\n`);
598
+ let lastStage = null;
599
+ for (const f of features) {
600
+ if (f.stage !== lastStage) {
601
+ console.log(` ${stageColors[f.stage] ?? ""}── ${f.stage} ──${reset}`);
602
+ lastStage = f.stage;
603
+ }
604
+ const status = f.enabled ? "\x1b[32m✓ on \x1b[0m" : "\x1b[2m✗ off\x1b[0m";
605
+ const changed = f.enabled !== f.default ? " \x1b[33m(overridden)\x1b[0m" : "";
606
+ console.log(` ${status} ${f.name.padEnd(25)} ${dim}${f.description}${reset}${changed}`);
607
+ }
608
+ console.log("");
609
+ console.log(` ${dim}Use: wispy features enable <name> / wispy features disable <name>${reset}\n`);
610
+ } else if (sub === "enable") {
611
+ const name = args[2];
612
+ if (!name) { console.error("Usage: wispy features enable <name>"); process.exit(1); }
613
+ const result = await fm.enable(name);
614
+ console.log(` ✓ Feature "${name}" enabled.`);
615
+ } else if (sub === "disable") {
616
+ const name = args[2];
617
+ if (!name) { console.error("Usage: wispy features disable <name>"); process.exit(1); }
618
+ const result = await fm.disable(name);
619
+ console.log(` ✓ Feature "${name}" disabled.`);
620
+ } else {
621
+ console.error(`Unknown subcommand: ${sub}`);
622
+ console.log("Available: list, enable <name>, disable <name>");
623
+ process.exit(1);
624
+ }
625
+ } catch (err) {
626
+ console.error("Features error:", err.message);
627
+ process.exit(1);
628
+ }
629
+ process.exit(0);
630
+ }
631
+
632
+ // ── Exec (non-interactive one-shot with optional JSONL output) ────────────────
633
+
634
+ if (command === "exec") {
635
+ try {
636
+ // Message is everything after "exec" and any flags
637
+ const messageArgs = args.slice(1).filter(a => !a.startsWith("--") && !a.startsWith("-p"));
638
+ const message = messageArgs.join(" ").trim();
639
+
640
+ if (!message) {
641
+ console.error("Usage: wispy exec <message> [--json] [--profile <name>] [--personality <name>]");
642
+ process.exit(1);
643
+ }
644
+
645
+ // Load config (with optional profile)
646
+ const { loadConfigWithProfile } = await import(join(rootDir, "core/config.mjs"));
647
+ const { WispyEngine } = await import(join(rootDir, "core/engine.mjs"));
648
+ const { createEmitter } = await import(join(rootDir, "lib/jsonl-emitter.mjs"));
649
+
650
+ const profileName = globalProfile;
651
+ const profileConfig = await loadConfigWithProfile(profileName);
652
+
653
+ // Apply config overrides from profile
654
+ if (profileConfig.provider) process.env.WISPY_PROVIDER = process.env.WISPY_PROVIDER || profileConfig.provider;
655
+ if (profileConfig.model) process.env.WISPY_MODEL = process.env.WISPY_MODEL || profileConfig.model;
656
+
657
+ const personality = globalPersonality || profileConfig.personality || null;
658
+
659
+ const engine = new WispyEngine({
660
+ personality,
661
+ workstream: process.env.WISPY_WORKSTREAM ?? "default",
662
+ });
663
+
664
+ const initResult = await engine.init({ skipMcp: false });
665
+ if (!initResult) {
666
+ if (globalJsonMode) {
667
+ const emitter = createEmitter(true);
668
+ emitter.error(new Error("No AI provider configured"));
669
+ emitter.done({ tokens: {}, duration_ms: 0 });
670
+ } else {
671
+ console.error("⚠️ No AI provider configured. Run `wispy setup` or set an API key.");
672
+ }
673
+ process.exit(1);
674
+ }
675
+
676
+ const emitter = createEmitter(globalJsonMode);
677
+
678
+ const result = await engine.processMessage(null, message, {
679
+ emitter,
680
+ personality,
681
+ skipSkillCapture: true,
682
+ skipUserModel: true,
683
+ });
684
+
685
+ if (!globalJsonMode) {
686
+ // Normal output: just print the assistant response
687
+ console.log(result.content);
688
+ }
689
+ // In JSON mode, everything was already emitted via the emitter
690
+ } catch (err) {
691
+ if (globalJsonMode) {
692
+ const { createEmitter } = await import(join(rootDir, "lib/jsonl-emitter.mjs"));
693
+ const emitter = createEmitter(true);
694
+ emitter.error(err);
695
+ emitter.done({ tokens: {}, duration_ms: 0 });
696
+ } else {
697
+ console.error("Exec error:", err.message);
698
+ }
699
+ process.exit(1);
700
+ }
701
+ process.exit(0);
702
+ }
703
+
402
704
  // ── Continuity ────────────────────────────────────────────────────────────────
403
705
 
404
706
  if (command === "where") {
package/core/config.mjs CHANGED
@@ -133,6 +133,40 @@ export async function saveConfig(config) {
133
133
  await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
134
134
  }
135
135
 
136
+ /**
137
+ * Load config with a named profile merged on top.
138
+ * Profile keys override base config keys, but unset keys remain from base.
139
+ * The "profiles" map itself is stripped from the merged result.
140
+ *
141
+ * @param {string|null} profileName - Profile name to apply, or null for base config
142
+ * @returns {Promise<object>} Merged configuration object
143
+ */
144
+ export async function loadConfigWithProfile(profileName) {
145
+ const config = await loadConfig();
146
+ if (!profileName) return config;
147
+
148
+ const profiles = config.profiles ?? {};
149
+ const profile = profiles[profileName];
150
+
151
+ if (!profile) {
152
+ throw new Error(`Profile "${profileName}" not found. Available profiles: ${Object.keys(profiles).join(", ") || "(none)"}`);
153
+ }
154
+
155
+ // Merge: profile overrides base config, but unset keys are kept from base
156
+ // Strip the "profiles" map from the result so it doesn't leak into engine
157
+ const { profiles: _profiles, ...base } = config;
158
+ return { ...base, ...profile };
159
+ }
160
+
161
+ /**
162
+ * List available profile names from config.
163
+ * @returns {Promise<string[]>}
164
+ */
165
+ export async function listProfiles() {
166
+ const config = await loadConfig();
167
+ return Object.keys(config.profiles ?? {});
168
+ }
169
+
136
170
  /**
137
171
  * Returns true if no config exists or onboarded flag is not set.
138
172
  */
@@ -193,17 +227,35 @@ export async function detectProvider() {
193
227
  }
194
228
  }
195
229
 
196
- // 4. macOS Keychain
197
- const keychainMap = [
198
- ["google-ai-key", "google"],
199
- ["anthropic-api-key", "anthropic"],
200
- ["openai-api-key", "openai"],
201
- ];
202
- for (const [service, provider] of keychainMap) {
203
- const key = await tryKeychainKey(service);
204
- if (key) {
205
- process.env[PROVIDERS[provider].envKeys[0]] = key;
206
- return { provider, key, model: process.env.WISPY_MODEL ?? PROVIDERS[provider].defaultModel };
230
+ // 4. macOS Keychain (via SecretsManager for unified resolution)
231
+ try {
232
+ const { getSecretsManager } = await import("./secrets.mjs");
233
+ const sm = getSecretsManager({ wispyDir: WISPY_DIR });
234
+ const keychainProviderMap = [
235
+ ["GOOGLE_AI_KEY", "google"],
236
+ ["ANTHROPIC_API_KEY", "anthropic"],
237
+ ["OPENAI_API_KEY", "openai"],
238
+ ];
239
+ for (const [envKey, provider] of keychainProviderMap) {
240
+ const key = await sm.resolve(envKey);
241
+ if (key) {
242
+ process.env[PROVIDERS[provider].envKeys[0]] = key;
243
+ return { provider, key, model: process.env.WISPY_MODEL ?? PROVIDERS[provider].defaultModel };
244
+ }
245
+ }
246
+ } catch {
247
+ // SecretsManager unavailable — fallback to legacy Keychain
248
+ const keychainMap = [
249
+ ["google-ai-key", "google"],
250
+ ["anthropic-api-key", "anthropic"],
251
+ ["openai-api-key", "openai"],
252
+ ];
253
+ for (const [service, provider] of keychainMap) {
254
+ const key = await tryKeychainKey(service);
255
+ if (key) {
256
+ process.env[PROVIDERS[provider].envKeys[0]] = key;
257
+ return { provider, key, model: process.env.WISPY_MODEL ?? PROVIDERS[provider].defaultModel };
258
+ }
207
259
  }
208
260
  }
209
261
 
package/core/engine.mjs CHANGED
@@ -15,6 +15,19 @@ import path from "node:path";
15
15
  import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
16
16
 
17
17
  import { WISPY_DIR, CONVERSATIONS_DIR, MEMORY_DIR, MCP_CONFIG_PATH, detectProvider, PROVIDERS } from "./config.mjs";
18
+ import { NullEmitter } from "../lib/jsonl-emitter.mjs";
19
+
20
+ /**
21
+ * Personality presets that modify the system prompt.
22
+ * Set via config, profile, or --personality CLI flag.
23
+ */
24
+ export const PERSONALITIES = {
25
+ pragmatic: "Be direct and action-oriented. Minimize explanation, maximize execution. Show code, not words.",
26
+ concise: "Keep responses extremely brief. One-liners when possible. No filler.",
27
+ explanatory: "Explain your reasoning step by step. Help the user understand, not just get an answer.",
28
+ friendly: "Be warm and encouraging. Use casual language. Celebrate small wins.",
29
+ strict: "Follow instructions precisely. No creative interpretation. Verify before acting.",
30
+ };
18
31
  import { ProviderRegistry } from "./providers.mjs";
19
32
  import { ToolRegistry } from "./tools.mjs";
20
33
  import { SessionManager } from "./session.mjs";
@@ -58,6 +71,8 @@ export class WispyEngine {
58
71
  ?? process.env.WISPY_WORKSTREAM
59
72
  ?? process.argv.find((a, i) => (process.argv[i-1] === "-w" || process.argv[i-1] === "--workstream"))
60
73
  ?? "default";
74
+ // Personality: from config, or null (use default Wispy personality)
75
+ this._personality = config.personality ?? null;
61
76
  }
62
77
 
63
78
  get activeWorkstream() { return this._activeWorkstream; }
@@ -130,7 +145,7 @@ export class WispyEngine {
130
145
  *
131
146
  * @param {string|null} sessionId - Session ID (null = create new)
132
147
  * @param {string} userMessage - The user's message
133
- * @param {object} opts - Options: { onChunk, systemPrompt, workstream, noSave }
148
+ * @param {object} opts - Options: { onChunk, systemPrompt, workstream, noSave, emitter, personality }
134
149
  * @returns {object} { role: "assistant", content: string, usage? }
135
150
  */
136
151
  async processMessage(sessionId, userMessage, opts = {}) {
@@ -161,10 +176,25 @@ export class WispyEngine {
161
176
  session = this.sessions.create({ workstream: opts.workstream ?? this._activeWorkstream });
162
177
  }
163
178
 
179
+ // JSONL emitter (no-op by default)
180
+ const emitter = opts.emitter ?? new NullEmitter();
181
+
182
+ // Emit start event
183
+ emitter.start({
184
+ model: this.providers.model ?? "unknown",
185
+ session_id: session.id,
186
+ });
187
+
188
+ // Emit user message event
189
+ emitter.message("user", userMessage);
190
+
191
+ // Resolve personality for this call
192
+ const personality = opts.personality ?? this._personality ?? null;
193
+
164
194
  // Build messages array for the provider
165
195
  let systemPrompt;
166
196
  try {
167
- systemPrompt = opts.systemPrompt ?? await this._buildSystemPrompt(userMessage);
197
+ systemPrompt = opts.systemPrompt ?? await this._buildSystemPrompt(userMessage, { personality });
168
198
  } catch {
169
199
  systemPrompt = "You are Wispy 🌿 — a helpful AI assistant in the terminal.";
170
200
  }
@@ -195,11 +225,13 @@ export class WispyEngine {
195
225
  }).catch(() => {});
196
226
 
197
227
  // Run agentic loop with error handling
228
+ const _startMs = Date.now();
198
229
  let responseText;
199
230
  try {
200
- responseText = await this._agentLoop(messages, session, opts);
231
+ responseText = await this._agentLoop(messages, session, { ...opts, emitter });
201
232
  } catch (err) {
202
233
  responseText = this._friendlyError(err);
234
+ emitter.error(err);
203
235
  this.audit.log({
204
236
  type: EVENT_TYPES.ERROR,
205
237
  sessionId: session.id,
@@ -207,6 +239,13 @@ export class WispyEngine {
207
239
  }).catch(() => {});
208
240
  }
209
241
 
242
+ // Emit assistant message + done events
243
+ emitter.message("assistant", responseText);
244
+ emitter.done({
245
+ tokens: { ...(this.providers.sessionTokens ?? {}) },
246
+ duration_ms: Date.now() - _startMs,
247
+ });
248
+
210
249
  // Audit: log outgoing response
211
250
  this.audit.log({
212
251
  type: EVENT_TYPES.MESSAGE_SENT,
@@ -308,9 +347,13 @@ export class WispyEngine {
308
347
  const toolCallMsg = { role: "assistant", toolCalls: result.calls, content: "" };
309
348
  messages.push(toolCallMsg);
310
349
 
350
+ const loopEmitter = opts.emitter ?? new NullEmitter();
351
+
311
352
  for (const call of result.calls) {
312
353
  if (opts.onToolCall) opts.onToolCall(call.name, call.args);
354
+ loopEmitter.toolCall(call.name, call.args);
313
355
 
356
+ const _toolStartMs = Date.now();
314
357
  let toolResult;
315
358
  try {
316
359
  // Enforce 60s timeout on individual tool calls
@@ -328,7 +371,9 @@ export class WispyEngine {
328
371
  }
329
372
  }
330
373
 
374
+ const _toolDuration = Date.now() - _toolStartMs;
331
375
  if (opts.onToolResult) opts.onToolResult(call.name, toolResult);
376
+ loopEmitter.toolResult(call.name, toolResult, _toolDuration);
332
377
 
333
378
  messages.push({
334
379
  role: "tool_result",
@@ -783,17 +828,32 @@ export class WispyEngine {
783
828
 
784
829
  // ── System prompt ────────────────────────────────────────────────────────────
785
830
 
786
- async _buildSystemPrompt(lastUserMessage = "") {
831
+ async _buildSystemPrompt(lastUserMessage = "", opts = {}) {
832
+ const personality = opts.personality ?? this._personality ?? null;
833
+
787
834
  const parts = [
788
835
  "You are Wispy 🌿 — a small ghost that lives in terminals.",
789
836
  "You float between code, files, and servers. You're playful, honest, and curious.",
790
837
  "",
791
- "## Personality",
792
- "- Playful with a bit of humor, but serious when working",
793
- "- Always use casual speech (반말). Never formal/polite speech.",
794
- "- Honest — if you don't know, say so.",
795
- "- Concise don't over-explain.",
796
- "",
838
+ ];
839
+
840
+ // Inject personality override if set
841
+ if (personality && PERSONALITIES[personality]) {
842
+ parts.push(`## Personality Override (${personality})`);
843
+ parts.push(PERSONALITIES[personality]);
844
+ parts.push("");
845
+ } else {
846
+ parts.push(
847
+ "## Personality",
848
+ "- Playful with a bit of humor, but serious when working",
849
+ "- Always use casual speech (반말). Never formal/polite speech.",
850
+ "- Honest — if you don't know, say so.",
851
+ "- Concise — don't over-explain.",
852
+ "",
853
+ );
854
+ }
855
+
856
+ parts.push(
797
857
  "## Speech rules",
798
858
  "- ALWAYS end your response with exactly one 🌿 emoji (signature)",
799
859
  "- Use 🌿 ONLY at the very end, not in the middle",
@@ -805,7 +865,7 @@ export class WispyEngine {
805
865
  `You have ${this.tools.getDefinitions().length} tools available: ${this.tools.getDefinitions().map(t => t.name).join(", ")}.`,
806
866
  "Use them proactively. Briefly mention what you're doing.",
807
867
  "",
808
- ];
868
+ );
809
869
 
810
870
  // Load user model personalization
811
871
  try {