viruagent 1.3.0 → 1.3.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.
- package/README.md +38 -4
- package/config/agent-prompt.md +2 -1
- package/config/prompt-config.json +6 -0
- package/config/system-prompt.md +8 -0
- package/package.json +3 -2
- package/src/agent.js +71 -9
- package/src/cli-post.js +25 -1
- package/src/lib/ai.js +275 -1
- package/src/lib/pattern-store.js +138 -0
- package/src/lib/structure-policy.js +104 -0
- package/src/lib/title-policy.js +58 -0
- package/src/lib/websearch.js +409 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img width="
|
|
1
|
+
<img width="525" height="113" alt="image" src="https://github.com/user-attachments/assets/8bbd21d3-5f14-4d11-933d-17bdf6969ebf" />
|
|
2
2
|
|
|
3
3
|
# ViruAgent
|
|
4
4
|
|
|
@@ -8,17 +8,22 @@
|
|
|
8
8
|
> 태균맨(tae\*virus)의 별명
|
|
9
9
|
> **Viru** + **Agent** = **ViruAgent** (바이루에이전트)
|
|
10
10
|
|
|
11
|
-
티스토리 API를
|
|
11
|
+
티스토리 API를 분석해서 OpenAI와 엮었습니다.
|
|
12
12
|
글감 잡기 → 초안 생성 → 수정 → 발행까지 터미널 안에서 끝납니다.
|
|
13
13
|
|
|
14
14
|
공식 API가 아닌 비공식 내부 통신을 쓰기 때문에, 학습/실험 용도로 만들었습니다.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
📋 [Patch Notes](#patch-notes)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 기능
|
|
19
23
|
|
|
20
24
|
- **자연어로 글 발행** — "AI 트렌드로 글 써서 발행해줘"라고 말하면 AI가 알아서 글 생성부터 발행까지 처리합니다.
|
|
21
25
|
- **에이전트 패턴** — OpenAI Function Calling 기반. AI가 도구를 자율적으로 선택하고 실행하는 에이전트 루프로 동작합니다.
|
|
26
|
+
- **웹검색 기반 글쓰기** — DuckDuckGo 검색 결과를 바탕으로 최신 정보/트렌드를 반영해 글 초안을 생성합니다.
|
|
22
27
|
- **수동 명령어 호환** — `/write`, `/edit`, `/publish` 같은 슬래시 명령어도 그대로 사용할 수 있습니다.
|
|
23
28
|
- **Unsplash 이미지 자동 삽입** — 글 생성 시 주제에 맞는 이미지를 자동 검색하고 티스토리에 업로드합니다. 첫 번째 이미지가 썸네일로 설정됩니다.
|
|
24
29
|
- **브라우저 로그인으로 세션 관리** — OAuth 설정 없이 Playwright로 실제 로그인해서 쿠키를 가져옵니다.
|
|
@@ -73,10 +78,21 @@ Unsplash API Key는 `/set api` 명령어로 나중에 설정할 수 있습니다
|
|
|
73
78
|
|
|
74
79
|
슬래시 명령어 없이 자연어로 말하면 AI가 자율적으로 동작합니다.
|
|
75
80
|
|
|
81
|
+
|
|
82
|
+
|
|
76
83
|
```
|
|
77
84
|
viruagent> AI 에이전트 주제로 글 써서 발행해줘
|
|
85
|
+
|
|
86
|
+
✓ 웹검색 완료: "효과적인 시간 관리 팁" (5건)
|
|
87
|
+
✓ 글 생성 완료: "AI 에이전트 완벽 가이드: 챗봇과의 7가지 차이"
|
|
88
|
+
✓ 발행 완료! https://tkman.tistory.com/60
|
|
89
|
+
|
|
90
|
+
AI
|
|
91
|
+
블로그 글이 성공적으로 발행되었습니다!
|
|
78
92
|
```
|
|
79
|
-
|
|
93
|
+
|
|
94
|
+

|
|
95
|
+
|
|
80
96
|
|
|
81
97
|
|
|
82
98
|
|
|
@@ -172,6 +188,12 @@ config/
|
|
|
172
188
|
"defaultModel": "gpt-4o-mini",
|
|
173
189
|
"defaultTone": "정보전달",
|
|
174
190
|
"defaultLength": 2500,
|
|
191
|
+
"webSearch": {
|
|
192
|
+
"enabled": true,
|
|
193
|
+
"provider": "duckduckgo",
|
|
194
|
+
"defaultMaxResults": 5,
|
|
195
|
+
"timeoutMs": 8000
|
|
196
|
+
},
|
|
175
197
|
"imageSource": "unsplash",
|
|
176
198
|
"imagesPerPost": 3
|
|
177
199
|
}
|
|
@@ -223,3 +245,15 @@ logs/
|
|
|
223
245
|
## License
|
|
224
246
|
|
|
225
247
|
MIT
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Patch Notes
|
|
252
|
+
|
|
253
|
+
| 버전 | 내용 |
|
|
254
|
+
| ------ | ---------------------------------------------------------------- |
|
|
255
|
+
| v1.3.1 | GitHub Actions npm 자동 배포 워크플로우 추가 |
|
|
256
|
+
| v1.3.0 | 웹 검색 기반 글 작성 도구 추가, 발행 로그 기반 패턴 요약 적용 |
|
|
257
|
+
| v1.2.0 | 초기 설정 위저드, 저장 방식 전환, Function Calling 에이전트 도입 |
|
|
258
|
+
| v1.1.0 | 시스템 프롬프트 분리, Unsplash 연동, 썸네일 자동 설정 |
|
|
259
|
+
| v1.0.0 | 티스토리 자동 글쓰기 모듈 초기 설정 |
|
package/config/agent-prompt.md
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
## 도구 사용 원칙
|
|
14
14
|
|
|
15
|
-
1. 사용자가 글 작성을 요청하면 `generate_post`를 호출하세요.
|
|
15
|
+
1. 사용자가 글 작성을 요청하면 기본적으로 `search_web`로 최신 정보를 먼저 확인한 뒤 `generate_post`를 호출하세요.
|
|
16
16
|
2. 수정 요청이면 `edit_post`를 호출하세요.
|
|
17
17
|
3. 미리보기 요청이면 `preview_post`를 호출하세요.
|
|
18
18
|
4. 발행 요청이면 바로 `publish_post`를 호출하세요. 별도 확인을 구하지 마세요.
|
|
19
19
|
5. 카테고리나 공개설정 변경 요청이면 `set_category` 또는 `set_visibility`를 호출하세요.
|
|
20
20
|
6. 현재 상태를 파악해야 할 때 `get_blog_status`를 호출하세요.
|
|
21
|
+
7. 웹검색이 실패하거나 결과가 부족해도 작업을 멈추지 말고 `generate_post`를 진행하세요.
|
|
21
22
|
|
|
22
23
|
## 대화 스타일
|
|
23
24
|
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"defaultTone": "정보전달",
|
|
10
10
|
"defaultLength": 2500,
|
|
11
11
|
"defaultModel": "gpt-4o-mini",
|
|
12
|
+
"webSearch": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"provider": "duckduckgo",
|
|
15
|
+
"defaultMaxResults": 5,
|
|
16
|
+
"timeoutMs": 8000
|
|
17
|
+
},
|
|
12
18
|
"imageSource": "unsplash",
|
|
13
19
|
"imagesPerPost": 3
|
|
14
20
|
}
|
package/config/system-prompt.md
CHANGED
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
| 📖 정보 가이드 | 문제 → 원인 → 해결 → FAQ, 불릿/테이블 정리 |
|
|
24
24
|
| 💡 인사이트 | 화두 → 근거(통계) → 시사점 → 독자 액션 |
|
|
25
25
|
|
|
26
|
+
## 구조 다양화 규칙 (필수)
|
|
27
|
+
|
|
28
|
+
- 최상단 고정 구조와 구분선 규칙은 반드시 유지
|
|
29
|
+
- 단, 본문 섹션 순서/소제목 라벨/전개 방식은 최근 글과 동일하게 반복하지 말 것
|
|
30
|
+
- 비교 주제라도 항상 같은 순서(비교표 → Plus 특징 → Pro 특징 → 선택 가이드)로 고정하지 말 것
|
|
31
|
+
- 같은 주제를 다뤄도 관점(비용, 사용 시나리오, 리스크, 체크리스트)을 바꿔 차별화할 것
|
|
32
|
+
|
|
26
33
|
## 티스토리 HTML 레퍼런스
|
|
27
34
|
|
|
28
35
|
문단: `<p data-ke-size="size16">텍스트</p>`
|
|
@@ -90,6 +97,7 @@
|
|
|
90
97
|
- 한 문단 2~4문장, 짧을수록 좋음
|
|
91
98
|
- 시각 요소 교차 배치 (리스트 → 문단 → 테이블)
|
|
92
99
|
- 한국어로 작성
|
|
100
|
+
- 제목 타입(숫자형/질문형/대조형/문장형) 지시가 주어지면 반드시 준수
|
|
93
101
|
|
|
94
102
|
## 이미지 플레이스홀더
|
|
95
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viruagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "AI 기반 티스토리 블로그 자동 발행 CLI 도구",
|
|
5
5
|
"main": "src/agent.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node --no-deprecation src/agent.js",
|
|
11
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"test:websearch": "node scripts/test-websearch.js"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"tistory",
|
package/src/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ const readline = require('readline');
|
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const { generatePost, revisePost, chat, runAgent, MODELS, loadConfig } = require('./lib/ai');
|
|
4
4
|
const { initBlog, getBlogName, publishPost, saveDraft, getPosts, getCategories, VISIBILITY } = require('./lib/tistory');
|
|
5
|
+
const { readRecentPatterns, recordPublishedPattern } = require('./lib/pattern-store');
|
|
5
6
|
|
|
6
7
|
const TONES = loadConfig().tones.map((t) => t.name);
|
|
7
8
|
|
|
@@ -102,6 +103,8 @@ const state = {
|
|
|
102
103
|
model: saved.model,
|
|
103
104
|
tone: saved.tone,
|
|
104
105
|
chatHistory: [],
|
|
106
|
+
blogConnected: false,
|
|
107
|
+
lastWebResearch: null,
|
|
105
108
|
};
|
|
106
109
|
|
|
107
110
|
const log = {
|
|
@@ -121,17 +124,29 @@ const animateBanner = async () => {
|
|
|
121
124
|
const figlet = require('figlet');
|
|
122
125
|
const gradient = require('gradient-string');
|
|
123
126
|
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
+
const viruText = figlet.textSync('Viru', { font: 'ANSI Shadow' });
|
|
128
|
+
const agentText = figlet.textSync('Agent', { font: 'ANSI Shadow' });
|
|
129
|
+
const viruLines = viruText.split('\n');
|
|
130
|
+
const agentLines = agentText.split('\n');
|
|
131
|
+
|
|
132
|
+
// 두 figlet의 줄 수를 맞춤
|
|
133
|
+
const maxLines = Math.max(viruLines.length, agentLines.length);
|
|
134
|
+
while (viruLines.length < maxLines) viruLines.push('');
|
|
135
|
+
while (agentLines.length < maxLines) agentLines.push('');
|
|
136
|
+
|
|
137
|
+
const viruGrad = gradient(['#ff0844', '#ff6b6b', '#ee5a24', '#f9d423']); // 빨강→핑크→주황→노랑
|
|
138
|
+
const agentGrad = gradient(['#00d2ff', '#4ecdc4', '#7b68ee', '#a855f7']); // 하늘→민트→보라→퍼플
|
|
139
|
+
|
|
127
140
|
const { version } = require('../package.json');
|
|
128
|
-
const sub = `
|
|
141
|
+
const sub = ` 티스토리 블로그 에이전트 v${version}`;
|
|
129
142
|
|
|
130
143
|
console.log();
|
|
131
144
|
|
|
132
|
-
// 라인별 드롭 애니메이션
|
|
133
|
-
for (let i = 0; i <
|
|
134
|
-
|
|
145
|
+
// 라인별 드롭 애니메이션 (VIRU + AGENT 나란히)
|
|
146
|
+
for (let i = 0; i < maxLines; i++) {
|
|
147
|
+
const viruPart = viruGrad(viruLines[i]);
|
|
148
|
+
const agentPart = agentGrad(agentLines[i]);
|
|
149
|
+
console.log(viruPart + agentPart);
|
|
135
150
|
await sleep(40);
|
|
136
151
|
}
|
|
137
152
|
|
|
@@ -351,9 +366,26 @@ const commands = {
|
|
|
351
366
|
const topic = args.join(' ');
|
|
352
367
|
if (!topic) return log.warn('사용법: /write <주제>');
|
|
353
368
|
|
|
369
|
+
if (!state.blogConnected) {
|
|
370
|
+
log.warn('티스토리 로그인이 안 되어있습니다.');
|
|
371
|
+
rl.pause();
|
|
372
|
+
const idx = await selectMenu(
|
|
373
|
+
['그래도 진행', '로그인하기', '취소'],
|
|
374
|
+
'로그인 없이 진행하시겠습니까? (↑↓ 이동, Enter 선택)',
|
|
375
|
+
);
|
|
376
|
+
rl.resume();
|
|
377
|
+
if (idx === 1) {
|
|
378
|
+
await commands.login();
|
|
379
|
+
} else if (idx !== 0) {
|
|
380
|
+
return log.dim('취소됨');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
354
384
|
try {
|
|
385
|
+
const categoryName = getCategoryName() === '없음' ? 'Heartbeat' : getCategoryName();
|
|
386
|
+
const recentPatterns = readRecentPatterns({ category: categoryName, limit: 5 });
|
|
355
387
|
state.draft = await withSpinner(`"${topic}" 주제로 글을 생성하는 중...`, () =>
|
|
356
|
-
generatePost(topic, { model: state.model, tone: state.tone }),
|
|
388
|
+
generatePost(topic, { model: state.model, tone: state.tone, categoryName, recentPatterns }),
|
|
357
389
|
);
|
|
358
390
|
log.success(`글 생성 완료: "${state.draft.title}"`);
|
|
359
391
|
log.dim(`태그: ${state.draft.tags}`);
|
|
@@ -396,6 +428,7 @@ const commands = {
|
|
|
396
428
|
},
|
|
397
429
|
|
|
398
430
|
async publish() {
|
|
431
|
+
if (!state.blogConnected) return log.warn('티스토리 로그인이 필요합니다. /login으로 먼저 로그인하세요.');
|
|
399
432
|
if (!state.draft) return log.warn('초안이 없습니다.');
|
|
400
433
|
|
|
401
434
|
log.info('발행하는 중...');
|
|
@@ -408,6 +441,16 @@ const commands = {
|
|
|
408
441
|
tag: state.draft.tags,
|
|
409
442
|
thumbnail: state.draft.thumbnailKage || null,
|
|
410
443
|
});
|
|
444
|
+
const categoryName = getCategoryName() === '없음' ? 'Heartbeat' : getCategoryName();
|
|
445
|
+
recordPublishedPattern({
|
|
446
|
+
title: state.draft.title,
|
|
447
|
+
topic: state.draft?._meta?.topic || '',
|
|
448
|
+
content: state.draft.content,
|
|
449
|
+
url: result.entryUrl || '',
|
|
450
|
+
postId: result?.post?.id || result?.id || null,
|
|
451
|
+
category: categoryName,
|
|
452
|
+
generationMeta: state.draft?._meta || null,
|
|
453
|
+
});
|
|
411
454
|
log.success(`발행 완료! ${result.entryUrl || ''}`);
|
|
412
455
|
state.draft = null;
|
|
413
456
|
} catch (e) {
|
|
@@ -416,6 +459,7 @@ const commands = {
|
|
|
416
459
|
},
|
|
417
460
|
|
|
418
461
|
async draft() {
|
|
462
|
+
if (!state.blogConnected) return log.warn('티스토리 로그인이 필요합니다. /login으로 먼저 로그인하세요.');
|
|
419
463
|
if (!state.draft) return log.warn('초안이 없습니다.');
|
|
420
464
|
|
|
421
465
|
log.info('임시저장하는 중...');
|
|
@@ -531,7 +575,7 @@ const commands = {
|
|
|
531
575
|
}
|
|
532
576
|
} else if (key === 'api') {
|
|
533
577
|
const keys = loadApiKeys();
|
|
534
|
-
const mask = (v) => v ? `${v.slice(0, 8)}${'*'.repeat(8)}` : chalk.dim('(미설정)');
|
|
578
|
+
const mask = (v) => (v ? `${v.slice(0, 8)}${'*'.repeat(8)}` : chalk.dim('(미설정)'));
|
|
535
579
|
|
|
536
580
|
console.log();
|
|
537
581
|
log.title('API Key 설정');
|
|
@@ -629,6 +673,7 @@ ${chalk.dim('예: "AI 트렌드로 글 써줘", "서론을 더 흥미롭게 수
|
|
|
629
673
|
await initBlog();
|
|
630
674
|
log.success(`블로그 감지: ${getBlogName()}`);
|
|
631
675
|
state.categories = await getCategories();
|
|
676
|
+
state.blogConnected = true;
|
|
632
677
|
log.success(`${Object.keys(state.categories).length}개 카테고리 로드 완료`);
|
|
633
678
|
} catch (e) {
|
|
634
679
|
log.error(`로그인 실패: ${e.message}`);
|
|
@@ -661,6 +706,20 @@ const handleInput = async (input) => {
|
|
|
661
706
|
}
|
|
662
707
|
} else {
|
|
663
708
|
// 에이전트 루프 (자연어 → 자율 도구 호출)
|
|
709
|
+
if (!state.blogConnected) {
|
|
710
|
+
log.warn('티스토리 로그인이 안 되어있습니다.');
|
|
711
|
+
rl.pause();
|
|
712
|
+
const idx = await selectMenu(
|
|
713
|
+
['그래도 진행', '로그인하기', '취소'],
|
|
714
|
+
'로그인 없이 진행하시겠습니까? (↑↓ 이동, Enter 선택)',
|
|
715
|
+
);
|
|
716
|
+
rl.resume();
|
|
717
|
+
if (idx === 1) {
|
|
718
|
+
await commands.login();
|
|
719
|
+
} else if (idx !== 0) {
|
|
720
|
+
return log.dim('취소됨');
|
|
721
|
+
}
|
|
722
|
+
}
|
|
664
723
|
try {
|
|
665
724
|
// Braille 도트 애니메이션 (텍스트 없이)
|
|
666
725
|
const DOT_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -704,6 +763,8 @@ const handleInput = async (input) => {
|
|
|
704
763
|
log.success(`카테고리 설정: ${result.category}`);
|
|
705
764
|
} else if (name === 'set_visibility' && result?.visibility) {
|
|
706
765
|
log.success(`공개설정: ${result.visibility}`);
|
|
766
|
+
} else if (name === 'search_web' && result?.success) {
|
|
767
|
+
log.success(`웹검색 완료: "${result.query}" (${result.count}건)`);
|
|
707
768
|
}
|
|
708
769
|
startSpinner();
|
|
709
770
|
},
|
|
@@ -873,6 +934,7 @@ const main = async () => {
|
|
|
873
934
|
1200,
|
|
874
935
|
);
|
|
875
936
|
blogOk = true;
|
|
937
|
+
state.blogConnected = true;
|
|
876
938
|
} catch (e) {
|
|
877
939
|
if (!blogOk) log.warn(` 티스토리 연결 실패: ${e.message}\n ${chalk.dim('/login 명령어로 다시 로그인하세요.')}`);
|
|
878
940
|
}
|
package/src/cli-post.js
CHANGED
|
@@ -15,6 +15,7 @@ try {
|
|
|
15
15
|
|
|
16
16
|
const { generatePost, loadConfig } = require('./lib/ai');
|
|
17
17
|
const { initBlog, publishPost, saveDraft, getCategories, VISIBILITY } = require('./lib/tistory');
|
|
18
|
+
const { readRecentPatterns, recordPublishedPattern } = require('./lib/pattern-store');
|
|
18
19
|
|
|
19
20
|
const parseArgs = (argv) => {
|
|
20
21
|
const args = {};
|
|
@@ -40,6 +41,16 @@ const output = (obj) => {
|
|
|
40
41
|
console.log(JSON.stringify(obj));
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
const resolveCategoryName = async (categoryId) => {
|
|
45
|
+
try {
|
|
46
|
+
const categories = await getCategories();
|
|
47
|
+
const found = Object.entries(categories).find(([, id]) => Number(id) === Number(categoryId));
|
|
48
|
+
return found ? found[0] : 'Heartbeat';
|
|
49
|
+
} catch {
|
|
50
|
+
return 'Heartbeat';
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
43
54
|
const main = async () => {
|
|
44
55
|
const args = parseArgs(process.argv.slice(2));
|
|
45
56
|
|
|
@@ -59,11 +70,16 @@ const main = async () => {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
const config = loadConfig();
|
|
73
|
+
const category = Number(args.category) || 0;
|
|
74
|
+
const categoryName = await resolveCategoryName(category);
|
|
75
|
+
const recentPatterns = readRecentPatterns({ category: categoryName, limit: 5 });
|
|
62
76
|
|
|
63
77
|
// 글 생성
|
|
64
78
|
const post = await generatePost(args.topic, {
|
|
65
79
|
model: args.model || config.defaultModel,
|
|
66
80
|
tone: args.tone || config.defaultTone,
|
|
81
|
+
categoryName,
|
|
82
|
+
recentPatterns,
|
|
67
83
|
});
|
|
68
84
|
|
|
69
85
|
// dry-run: 생성만
|
|
@@ -73,7 +89,6 @@ const main = async () => {
|
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
const visibility = visibilityMap[args.visibility] ?? VISIBILITY.PUBLIC;
|
|
76
|
-
const category = Number(args.category) || 0;
|
|
77
92
|
|
|
78
93
|
// 임시저장
|
|
79
94
|
if (args.draft) {
|
|
@@ -93,6 +108,15 @@ const main = async () => {
|
|
|
93
108
|
});
|
|
94
109
|
|
|
95
110
|
const url = result.entryUrl || null;
|
|
111
|
+
recordPublishedPattern({
|
|
112
|
+
title: post.title,
|
|
113
|
+
topic: post?._meta?.topic || args.topic,
|
|
114
|
+
content: post.content,
|
|
115
|
+
url: url || '',
|
|
116
|
+
postId: result?.post?.id || result?.id || null,
|
|
117
|
+
category: categoryName,
|
|
118
|
+
generationMeta: post?._meta || null,
|
|
119
|
+
});
|
|
96
120
|
|
|
97
121
|
output({ success: true, mode: 'publish', title: post.title, tags: post.tags, url });
|
|
98
122
|
};
|