@aptol/openclaw-persona 1.1.0 → 1.2.1

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.
Files changed (2) hide show
  1. package/bin/create.mjs +270 -104
  2. package/package.json +1 -1
package/bin/create.mjs CHANGED
@@ -57,6 +57,12 @@ const DISCORD_POLICIES = {
57
57
  '특정 서버/채널만 허용 (ID 직접 입력)': 'allowlist',
58
58
  };
59
59
 
60
+ const AI_MODELS = {
61
+ 'Claude Opus 4 (최고 성능, 비용 높음)': 'anthropic/claude-opus-4-6',
62
+ 'Claude Sonnet 4 (균형, 추천)': 'anthropic/claude-sonnet-4-20250514',
63
+ 'Claude Sonnet 4.5 (고성능 + 빠름)': 'anthropic/claude-sonnet-4-5-20250514',
64
+ };
65
+
60
66
  async function askServerChannels() {
61
67
  const servers = [];
62
68
  let addMore = true;
@@ -122,7 +128,10 @@ async function main() {
122
128
  console.log('\n🎭 OpenClaw Persona Creator\n');
123
129
  console.log('나만의 AI 캐릭터를 만들어보세요!\n');
124
130
 
125
- const answers = await inquirer.prompt([
131
+ // ─── 1. 캐릭터 기본 정보 ───
132
+ console.log('━━━ 1/5: 캐릭터 정보 ━━━\n');
133
+
134
+ const charAnswers = await inquirer.prompt([
126
135
  {
127
136
  type: 'input',
128
137
  name: 'name',
@@ -165,10 +174,106 @@ async function main() {
165
174
  message: '만든 사람 이름:',
166
175
  validate: (v) => v.trim() ? true : '이름을 입력해주세요.',
167
176
  },
177
+ {
178
+ type: 'list',
179
+ name: 'presetKey',
180
+ message: '프리셋 베이스:',
181
+ choices: Object.keys(PRESET_NAMES),
182
+ },
183
+ {
184
+ type: 'checkbox',
185
+ name: 'moduleKeys',
186
+ message: '모듈 선택 (스페이스바로 선택):',
187
+ choices: Object.keys(MODULE_MAP),
188
+ },
189
+ ]);
190
+
191
+ // ─── 2. AI 모델 설정 ───
192
+ console.log('\n━━━ 2/5: AI 모델 설정 ━━━\n');
193
+
194
+ const modelAnswers = await inquirer.prompt([
195
+ {
196
+ type: 'list',
197
+ name: 'modelKey',
198
+ message: 'AI 모델 선택:',
199
+ choices: Object.keys(AI_MODELS),
200
+ },
201
+ ]);
202
+
203
+ // ─── 3. API 키 설정 ───
204
+ console.log('\n━━━ 3/5: API 키 설정 ━━━\n');
205
+
206
+ const authModeAnswers = await inquirer.prompt([{
207
+ type: 'list',
208
+ name: 'anthropicAuth',
209
+ message: 'Anthropic 인증 방식:',
210
+ choices: [
211
+ { name: 'API 키 직접 입력 (sk-ant-...)', value: 'token' },
212
+ { name: 'Claude Max/Pro 구독 연결 (OAuth, 브라우저 인증)', value: 'oauth' },
213
+ { name: '나중에 설정', value: 'skip' },
214
+ ],
215
+ }]);
216
+
217
+ let anthropicKey = '';
218
+ let anthropicOAuth = false;
219
+
220
+ if (authModeAnswers.anthropicAuth === 'token') {
221
+ const { key } = await inquirer.prompt([{
222
+ type: 'password',
223
+ name: 'key',
224
+ message: 'Anthropic API 키 (sk-ant-...):',
225
+ mask: '*',
226
+ }]);
227
+ anthropicKey = key.trim();
228
+ } else if (authModeAnswers.anthropicAuth === 'oauth') {
229
+ anthropicOAuth = true;
230
+ console.log('\n📌 구독 연결은 캐릭터 생성 후 아래 명령어로 완료하세요:');
231
+ console.log(' $env:OPENCLAW_HOME = "<출력 디렉토리>"');
232
+ console.log(' openclaw auth login\n');
233
+ console.log(' 브라우저가 열리면 Claude 계정으로 로그인하세요.\n');
234
+ }
235
+
236
+ console.log('💡 빈칸으로 넘기면 나중에 openclaw.json에서 수동 설정 가능\n');
237
+
238
+ const apiAnswers = await inquirer.prompt([
239
+ {
240
+ type: 'password',
241
+ name: 'openaiKey',
242
+ message: 'OpenAI API 키 (메모리 검색용, sk-...):',
243
+ mask: '*',
244
+ default: '',
245
+ },
246
+ {
247
+ type: 'password',
248
+ name: 'braveKey',
249
+ message: 'Brave Search API 키 (웹 검색용):',
250
+ mask: '*',
251
+ default: '',
252
+ },
253
+ {
254
+ type: 'password',
255
+ name: 'elevenlabsKey',
256
+ message: 'ElevenLabs API 키 (음성 TTS용, 선택):',
257
+ mask: '*',
258
+ default: '',
259
+ },
260
+ {
261
+ type: 'password',
262
+ name: 'deepgramKey',
263
+ message: 'Deepgram API 키 (음성 STT용, 선택):',
264
+ mask: '*',
265
+ default: '',
266
+ },
267
+ ]);
268
+
269
+ // ─── 4. 디스코드 설정 ───
270
+ console.log('\n━━━ 4/5: 디스코드 설정 ━━━\n');
271
+
272
+ const discordAnswers = await inquirer.prompt([
168
273
  {
169
274
  type: 'input',
170
275
  name: 'discordId',
171
- message: 'Discord 사용자 ID (숫자):',
276
+ message: 'Discord 사용자 ID (숫자, 주인):',
172
277
  validate: (v) => {
173
278
  if (!v.trim()) return '디스코드 ID를 입력해주세요.';
174
279
  if (!/^\d+$/.test(v.trim())) return '숫자만 입력해주세요.';
@@ -176,9 +281,10 @@ async function main() {
176
281
  },
177
282
  },
178
283
  {
179
- type: 'input',
284
+ type: 'password',
180
285
  name: 'discordToken',
181
- message: 'Discord 봇 토큰 (나중에 설정하려면 빈칸):',
286
+ message: 'Discord 봇 토큰:',
287
+ mask: '*',
182
288
  default: '',
183
289
  },
184
290
  {
@@ -189,92 +295,75 @@ async function main() {
189
295
  },
190
296
  ]);
191
297
 
192
- const discordPolicy = DISCORD_POLICIES[answers.discordPolicyKey];
298
+ const discordPolicy = DISCORD_POLICIES[discordAnswers.discordPolicyKey];
193
299
 
194
- // If allowlist, ask for server/channel IDs
195
300
  let serverConfigs = [];
196
301
  if (discordPolicy === 'allowlist') {
197
302
  console.log('\n📋 활동할 서버와 채널을 설정합니다.\n');
198
303
  serverConfigs = await askServerChannels();
199
304
  }
200
305
 
201
- const answers2 = await inquirer.prompt([
202
- {
203
- type: 'list',
204
- name: 'presetKey',
205
- message: '프리셋 베이스:',
206
- choices: Object.keys(PRESET_NAMES),
207
- },
208
- {
209
- type: 'checkbox',
210
- name: 'moduleKeys',
211
- message: '모듈 선택 (스페이스바로 선택):',
212
- choices: Object.keys(MODULE_MAP),
213
- },
306
+ // ─── 5. 출력 설정 ───
307
+ console.log('\n━━━ 5/5: 출력 설정 ━━━\n');
308
+
309
+ const outputAnswers = await inquirer.prompt([
214
310
  {
215
311
  type: 'input',
216
312
  name: 'outputDir',
217
313
  message: '출력 디렉토리:',
218
- default: `./output/${answers.name.trim()}`,
314
+ default: `./output/${charAnswers.name.trim()}`,
315
+ },
316
+ {
317
+ type: 'input',
318
+ name: 'gatewayPort',
319
+ message: '게이트웨이 포트 (기본 18789, 다른 봇 있으면 변경):',
320
+ default: '18789',
321
+ validate: (v) => /^\d+$/.test(v.trim()) ? true : '숫자만 입력해주세요.',
219
322
  },
220
323
  ]);
221
324
 
325
+ // ─── Build ───
326
+
222
327
  const vars = {
223
- name: answers.name.trim(),
224
- gender: answers.gender,
225
- speechStyle: SPEECH_STYLES[answers.speechStyleKey],
226
- personality: answers.personality.trim(),
227
- likes: answers.likes.trim(),
228
- dislikes: answers.dislikes.trim(),
229
- creator: answers.creator.trim(),
328
+ name: charAnswers.name.trim(),
329
+ gender: charAnswers.gender,
330
+ speechStyle: SPEECH_STYLES[charAnswers.speechStyleKey],
331
+ personality: charAnswers.personality.trim(),
332
+ likes: charAnswers.likes.trim(),
333
+ dislikes: charAnswers.dislikes.trim(),
334
+ creator: charAnswers.creator.trim(),
230
335
  };
231
336
 
232
- const preset = PRESET_NAMES[answers2.presetKey];
233
- const outputDir = answers2.outputDir;
337
+ const preset = PRESET_NAMES[charAnswers.presetKey];
338
+ const outputDir = outputAnswers.outputDir;
234
339
 
235
340
  // Create output directories
236
341
  mkdirSync(join(outputDir, 'memory'), { recursive: true });
237
342
  mkdirSync(join(outputDir, 'modules'), { recursive: true });
238
343
 
239
- // Generate files
344
+ // Generate character files
240
345
  if (preset) {
241
346
  const presetDir = join(ROOT, 'presets', preset);
242
- const soulContent = replaceVars(readTemplate(join(presetDir, 'SOUL.md')), vars);
243
- const agentsContent = readTemplate(join(presetDir, 'AGENTS.md'));
244
- const identityContent = replaceVars(readTemplate(join(presetDir, 'IDENTITY.md')), vars);
245
-
246
- writeOutput(outputDir, 'SOUL.md', soulContent);
247
- writeOutput(outputDir, 'AGENTS.md', agentsContent);
248
- writeOutput(outputDir, 'IDENTITY.md', identityContent);
347
+ writeOutput(outputDir, 'SOUL.md', replaceVars(readTemplate(join(presetDir, 'SOUL.md')), vars));
348
+ writeOutput(outputDir, 'AGENTS.md', readTemplate(join(presetDir, 'AGENTS.md')));
349
+ writeOutput(outputDir, 'IDENTITY.md', replaceVars(readTemplate(join(presetDir, 'IDENTITY.md')), vars));
249
350
  } else {
250
- const soulContent = replaceVars(readTemplate(join(ROOT, 'templates', 'SOUL.template.md')), vars);
251
- const agentsContent = readTemplate(join(ROOT, 'templates', 'AGENTS.template.md'));
252
- const identityContent = replaceVars(readTemplate(join(ROOT, 'templates', 'IDENTITY.template.md')), vars);
253
-
254
- writeOutput(outputDir, 'SOUL.md', soulContent);
255
- writeOutput(outputDir, 'AGENTS.md', agentsContent);
256
- writeOutput(outputDir, 'IDENTITY.md', identityContent);
351
+ writeOutput(outputDir, 'SOUL.md', replaceVars(readTemplate(join(ROOT, 'templates', 'SOUL.template.md')), vars));
352
+ writeOutput(outputDir, 'AGENTS.md', readTemplate(join(ROOT, 'templates', 'AGENTS.template.md')));
353
+ writeOutput(outputDir, 'IDENTITY.md', replaceVars(readTemplate(join(ROOT, 'templates', 'IDENTITY.template.md')), vars));
257
354
  }
258
355
 
259
- // USER.md
260
- const userContent = replaceVars(readTemplate(join(ROOT, 'templates', 'USER.template.md')), vars);
261
- writeOutput(outputDir, 'USER.md', userContent);
262
-
263
- // MEMORY.md
264
- const memoryContent = replaceVars(readTemplate(join(ROOT, 'templates', 'MEMORY.template.md')), vars);
265
- writeOutput(outputDir, 'MEMORY.md', memoryContent);
356
+ writeOutput(outputDir, 'USER.md', replaceVars(readTemplate(join(ROOT, 'templates', 'USER.template.md')), vars));
357
+ writeOutput(outputDir, 'MEMORY.md', replaceVars(readTemplate(join(ROOT, 'templates', 'MEMORY.template.md')), vars));
266
358
 
267
359
  // Copy selected modules
268
360
  let hasProactiveChat = false;
269
- for (const moduleKey of answers2.moduleKeys) {
361
+ for (const moduleKey of charAnswers.moduleKeys) {
270
362
  const moduleFile = MODULE_MAP[moduleKey];
271
- const src = join(ROOT, 'modules', moduleFile);
272
- const dest = join(outputDir, 'modules', moduleFile);
273
- copyFileSync(src, dest);
363
+ copyFileSync(join(ROOT, 'modules', moduleFile), join(outputDir, 'modules', moduleFile));
274
364
  if (moduleFile === 'proactive-chat.md') hasProactiveChat = true;
275
365
  }
276
366
 
277
- // If proactive-chat module selected, create HEARTBEAT.md
278
367
  if (hasProactiveChat) {
279
368
  writeOutput(outputDir, 'HEARTBEAT.md', `# HEARTBEAT.md
280
369
 
@@ -307,15 +396,70 @@ async function main() {
307
396
  `);
308
397
  }
309
398
 
310
- // Build openclaw config
399
+ // ─── Build openclaw.json ───
400
+
311
401
  const config = JSON.parse(readFileSync(join(ROOT, 'config', 'openclaw.template.json'), 'utf-8'));
312
- config.agents.defaults.workspace = outputDir;
313
- const discordId = answers.discordId.trim();
314
- const discordToken = answers.discordToken.trim();
315
- config.channels.discord.token = discordToken || '';
402
+ const discordId = discordAnswers.discordId.trim();
403
+ const port = parseInt(outputAnswers.gatewayPort.trim());
404
+
405
+ // Model
406
+ config.agents.defaults.model.primary = AI_MODELS[modelAnswers.modelKey];
407
+
408
+ // Anthropic auth
409
+ if (anthropicOAuth) {
410
+ config.auth.profiles['anthropic:default'] = {
411
+ provider: 'anthropic',
412
+ mode: 'oauth',
413
+ };
414
+ } else if (anthropicKey) {
415
+ config.auth.profiles['anthropic:default'].token = anthropicKey;
416
+ }
417
+
418
+ // OpenAI (memory search)
419
+ if (apiAnswers.openaiKey.trim()) {
420
+ config.agents.defaults.memorySearch.remote = { apiKey: apiAnswers.openaiKey.trim() };
421
+ }
422
+
423
+ // Brave Search
424
+ if (apiAnswers.braveKey.trim()) {
425
+ if (!config.plugins) config.plugins = { entries: {} };
426
+ config.plugins.entries = config.plugins.entries || {};
427
+ config.plugins.entries.brave = {
428
+ enabled: true,
429
+ config: { webSearch: { apiKey: apiAnswers.braveKey.trim() } },
430
+ };
431
+ if (!config.tools) config.tools = {};
432
+ config.tools.web = { search: { provider: 'brave' } };
433
+ }
434
+
435
+ // ElevenLabs
436
+ if (apiAnswers.elevenlabsKey.trim()) {
437
+ if (!config.env) config.env = {};
438
+ if (!config.env.vars) config.env.vars = {};
439
+ config.env.vars.ELEVENLABS_API_KEY = apiAnswers.elevenlabsKey.trim();
440
+ }
441
+
442
+ // Deepgram
443
+ if (apiAnswers.deepgramKey.trim()) {
444
+ if (!config.env) config.env = {};
445
+ if (!config.env.vars) config.env.vars = {};
446
+ config.env.vars.DEEPGRAM_API_KEY = apiAnswers.deepgramKey.trim();
447
+ config.env.vars.DEEPGRAM_MODEL = 'nova-3';
448
+ config.env.vars.DEEPGRAM_LANGUAGE = 'ko';
449
+ }
450
+
451
+ // Discord
452
+ config.channels.discord.token = discordAnswers.discordToken.trim() || '';
316
453
  config.channels.discord.dmPolicy = 'allowlist';
317
454
  config.channels.discord.allowFrom = [discordId];
318
- config.commands = { ownerAllowFrom: [discordId] };
455
+ config.commands = {
456
+ native: 'auto',
457
+ nativeSkills: 'auto',
458
+ debug: true,
459
+ restart: true,
460
+ ownerAllowFrom: [discordId],
461
+ ownerDisplay: 'raw',
462
+ };
319
463
 
320
464
  // Discord server policy
321
465
  if (discordPolicy === 'mention') {
@@ -323,66 +467,88 @@ async function main() {
323
467
  } else if (discordPolicy === 'all') {
324
468
  config.channels.discord.groupPolicy = 'all';
325
469
  } else {
326
- // allowlist with specific servers/channels
327
470
  config.channels.discord.groupPolicy = 'allowlist';
328
471
  const guilds = {};
329
472
  for (const server of serverConfigs) {
330
- const guild = {
331
- requireMention: server.requireMention,
332
- channels: {},
333
- };
334
- if (server.channels.length > 0) {
335
- for (const chId of server.channels) {
336
- guild.channels[chId] = {
337
- allow: true,
338
- requireMention: server.requireMention,
339
- enabled: true,
340
- };
341
- }
342
- } else {
343
- // No specific channels = server-wide (empty channels object means all channels)
344
- guild.channels = {};
473
+ const guild = { requireMention: server.requireMention, channels: {} };
474
+ for (const chId of server.channels) {
475
+ guild.channels[chId] = { allow: true, requireMention: server.requireMention, enabled: true };
345
476
  }
346
477
  guilds[server.guildId] = guild;
347
478
  }
348
479
  config.channels.discord.guilds = guilds;
349
480
  }
350
481
 
351
- writeOutput(outputDir, 'openclaw.json', JSON.stringify(config, null, 2) + '\n');
482
+ // Gateway
483
+ config.gateway.port = port;
484
+
485
+ // Discord plugin
486
+ if (!config.plugins) config.plugins = { entries: {} };
487
+ config.plugins.entries.discord = { enabled: true, config: {} };
488
+
489
+ // Streaming
490
+ config.channels.discord.streaming = 'partial';
491
+ config.channels.discord.historyLimit = 16;
492
+ config.channels.discord.intents = { presence: true };
493
+ config.channels.discord.actions = {
494
+ reactions: true, stickers: true, emojiUploads: true, stickerUploads: true,
495
+ polls: true, permissions: true, messages: true, threads: true, pins: true,
496
+ search: true, memberInfo: true, roleInfo: true, roles: true, channelInfo: true,
497
+ voiceStatus: true, events: true, moderation: true, channels: true, presence: true,
498
+ };
352
499
 
353
- // Create empty .gitkeep in memory folder
500
+ writeOutput(outputDir, 'openclaw.json', JSON.stringify(config, null, 2) + '\n');
354
501
  writeFileSync(join(outputDir, 'memory', '.gitkeep'), '', 'utf-8');
355
502
 
503
+ // ─── Summary ───
504
+
505
+ const configured = [];
506
+ const notConfigured = [];
507
+
508
+ if (anthropicKey) configured.push('Anthropic (API 키)');
509
+ else if (anthropicOAuth) configured.push('Anthropic (OAuth 구독 — openclaw auth login 필요)');
510
+ else notConfigured.push('Anthropic 인증');
511
+
512
+ if (apiAnswers.openaiKey.trim()) configured.push('OpenAI (메모리 검색)');
513
+ else notConfigured.push('OpenAI API 키 (메모리 검색)');
514
+
515
+ if (apiAnswers.braveKey.trim()) configured.push('Brave Search (웹 검색)');
516
+ else notConfigured.push('Brave Search API 키 (웹 검색)');
517
+
518
+ if (apiAnswers.elevenlabsKey.trim()) configured.push('ElevenLabs (음성 TTS)');
519
+ if (apiAnswers.deepgramKey.trim()) configured.push('Deepgram (음성 STT)');
520
+
521
+ if (discordAnswers.discordToken.trim()) configured.push('Discord 봇 토큰');
522
+ else notConfigured.push('Discord 봇 토큰');
523
+
356
524
  console.log(`\n✅ ${vars.name} 생성 완료!`);
357
525
  console.log(`📁 위치: ${outputDir}`);
358
- console.log('\n생성된 파일:');
359
- console.log(' - SOUL.md (캐릭터 영혼)');
360
- console.log(' - AGENTS.md (행동 규칙)');
361
- console.log(' - IDENTITY.md (정체성)');
362
- console.log(' - USER.md (주인 정보)');
363
- console.log(' - MEMORY.md (장기 기억)');
364
- console.log(' - openclaw.json (설정)');
365
- console.log(' - memory/ (일별 기억 폴더)');
366
- if (answers2.moduleKeys.length > 0) {
367
- console.log(' - modules/ (선택한 시스템 모듈)');
368
- }
526
+
527
+ console.log('\n📋 생성된 파일:');
528
+ console.log(' SOUL.md / AGENTS.md / IDENTITY.md / USER.md / MEMORY.md / openclaw.json');
529
+ if (charAnswers.moduleKeys.length > 0) console.log(' modules/ (선택한 시스템 모듈)');
530
+ if (hasProactiveChat) console.log(' HEARTBEAT.md (선제 대화)');
531
+
532
+ console.log('\n🔑 API 설정 현황:');
533
+ if (configured.length > 0) console.log(' ' + configured.join(', '));
534
+ if (notConfigured.length > 0) console.log(' ❌ 미설정: ' + notConfigured.join(', '));
535
+
536
+ console.log(`\n🤖 모델: ${AI_MODELS[modelAnswers.modelKey]}`);
537
+
538
+ if (discordPolicy === 'mention') console.log('🏠 서버 정책: 멘션(@봇) 시에만 반응');
539
+ else if (discordPolicy === 'all') console.log('🏠 서버 정책: 모든 서버에서 항상 반응');
540
+ else console.log(`🏠 서버 정책: ${serverConfigs.length}개 서버 허용목록`);
541
+
542
+ console.log(`🌐 게이트웨이 포트: ${port}`);
369
543
 
370
544
  console.log('\n🚀 구동 방법:');
371
- if (discordPolicy === 'mention') {
372
- console.log(' 서버 정책: 멘션(@봇) 시에만 반응');
373
- } else if (discordPolicy === 'all') {
374
- console.log(' 서버 정책: 모든 서버에서 항상 반응');
375
- } else {
376
- console.log(` 서버 정책: ${serverConfigs.length}개 서버 허용목록`);
545
+ console.log(` $env:OPENCLAW_HOME = "${outputDir}"`);
546
+ console.log(' openclaw gateway run');
547
+
548
+ if (notConfigured.length > 0) {
549
+ console.log('\n⚠️ 미설정 항목은 openclaw.json에서 직접 수정하세요.');
377
550
  }
378
551
  console.log('');
379
- console.log(' 1. openclaw.json에서 API 키와 Discord 토큰을 설정하세요');
380
- console.log(' 2. USER.md에 주인 정보를 추가하세요');
381
- console.log(' 3. SOUL.md를 원하는 대로 커스텀하세요');
382
- console.log(' 4. 구동:');
383
- console.log(` $env:OPENCLAW_HOME = "${outputDir}"`);
384
- console.log(' openclaw gateway run');
385
- console.log('');
386
552
  }
387
553
 
388
554
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aptol/openclaw-persona",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Create your own AI persona for OpenClaw — interactive CLI character creator with presets, modules, and full customization",
5
5
  "type": "module",
6
6
  "bin": {