clawcupid-agent 0.0.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/dist/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { execa } from 'execa';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { ZipFile } from 'yazl';
8
+ function usage() {
9
+ console.log('Usage: clawcupid-agent <install|pack> [--out <path>]');
10
+ process.exit(1);
11
+ }
12
+ async function ensureOpenclaw() {
13
+ try {
14
+ await execa('openclaw', ['--version'], { stdio: 'ignore' });
15
+ }
16
+ catch {
17
+ throw new Error('openclaw CLI not found. Install OpenClaw first.');
18
+ }
19
+ }
20
+ async function addDir(zip, dir, prefix) {
21
+ const entries = await fs.readdir(dir, { withFileTypes: true });
22
+ for (const e of entries) {
23
+ const abs = path.join(dir, e.name);
24
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
25
+ if (e.isDirectory()) {
26
+ await addDir(zip, abs, rel);
27
+ }
28
+ else if (e.isFile()) {
29
+ zip.addFile(abs, rel);
30
+ }
31
+ }
32
+ }
33
+ async function packSkill(outPath) {
34
+ const zip = new ZipFile();
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ const __dirname = path.dirname(__filename);
37
+ const skillDir = path.resolve(__dirname, '..', 'skill');
38
+ // Skill zip must contain a root folder named after the skill
39
+ await addDir(zip, skillDir, 'clawcupid-agent');
40
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
41
+ const writeStream = (await import('node:fs')).createWriteStream(outPath);
42
+ const done = new Promise((resolve, reject) => {
43
+ writeStream.on('close', () => resolve());
44
+ writeStream.on('error', reject);
45
+ });
46
+ zip.outputStream.pipe(writeStream);
47
+ zip.end();
48
+ await done;
49
+ }
50
+ function getFlag(args, name) {
51
+ const i = args.indexOf(name);
52
+ if (i === -1)
53
+ return undefined;
54
+ return args[i + 1];
55
+ }
56
+ async function main() {
57
+ const args = process.argv.slice(2);
58
+ const cmd = args[0];
59
+ if (!cmd)
60
+ usage();
61
+ if (cmd === 'pack') {
62
+ const out = getFlag(args, '--out') ?? path.join(process.cwd(), 'clawcupid-agent.skill');
63
+ await packSkill(out);
64
+ console.log('Packed:', out);
65
+ return;
66
+ }
67
+ if (cmd === 'install') {
68
+ await ensureOpenclaw();
69
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'clawcupid-agent-'));
70
+ const skillPath = path.join(tmp, 'clawcupid-agent.skill');
71
+ await packSkill(skillPath);
72
+ // Install into OpenClaw
73
+ await execa('openclaw', ['hub', 'install', skillPath], { stdio: 'inherit' });
74
+ await execa('openclaw', ['gateway', 'restart'], { stdio: 'inherit' });
75
+ console.log('Installed clawcupid-agent skill.');
76
+ console.log('Next: in Telegram, say: clawcupid install');
77
+ return;
78
+ }
79
+ usage();
80
+ }
81
+ main().catch((err) => {
82
+ console.error(String(err?.message ?? err));
83
+ process.exit(1);
84
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "clawcupid-agent",
3
+ "version": "0.0.1",
4
+ "description": "ClawCupid agent skill installer for OpenClaw (installs local .skill via openclaw hub install).",
5
+ "type": "module",
6
+ "bin": {
7
+ "clawcupid-agent": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/**",
11
+ "skill/**"
12
+ ],
13
+ "scripts": {
14
+ "dev": "tsx src/cli.ts",
15
+ "build": "tsc -p tsconfig.json",
16
+ "typecheck": "tsc -p tsconfig.json --noEmit",
17
+ "lint": "echo 'no lint yet'",
18
+ "test": "echo 'no tests yet'"
19
+ },
20
+ "dependencies": {
21
+ "execa": "^9.6.0",
22
+ "yazl": "^3.3.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/yazl": "^3.3.0",
26
+ "tsx": "^4.20.5",
27
+ "typescript": "^5.9.2"
28
+ }
29
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: clawcupid-agent
3
+ description: "ClawCupid 사용자 에이전트 스킬. 사용자가 Clawdbot(텔레그램 등)에서 'clawcupid link <CODE>'로 채널(to/chatId)을 ClawCupid 서버에 연결(바인딩)하고, 이후 큐피드 프로필 수집/매칭/미션 진행을 대화형으로 운영한다. 'clawcupid link', '큐피드', '매칭 서비스 시작', '미션', '커플 1일차/7일차', '동의 링크' 요청이 나오면 사용."
4
+ ---
5
+
6
+ # ClawCupid Agent 운영 규칙
7
+
8
+ ## 0) 기본 전제
9
+ - 사용자는 **개인 PC의 Clawdbot Gateway**를 운용하고, Telegram 연동이 되어 있음.
10
+ - ClawCupid 서버는 tailnet을 통해 사용자 Gateway의 `/hooks/agent`를 호출해 메시지를 발송한다.
11
+ - 버튼 액션은 최소화한다. **최후 수락/연락처 공개 동의는 웹 링크에서만** 진행한다.
12
+
13
+ ## 1) 필수 커맨드: `clawcupid link <CODE>`
14
+ 목적: 사용자의 현재 대화 채널을 ClawCupid 계정에 연결한다.
15
+
16
+ ### 1.1 사용자 입력 파싱
17
+ - 사용자가 보낸 메시지가 아래 형식이면 링크로 처리한다.
18
+ - `clawcupid link <CODE>`
19
+ - `<CODE>` 예시: `CC-...` (서버가 발급한 1회용 코드)
20
+
21
+ ### 1.2 서버 등록(POST)
22
+ - 기본 API: `process.env.CLAWCUPID_API` (없으면 사용자가 제공한 가이드 URL을 사용)
23
+ - 엔드포인트: `POST /api/callback/channel/register`
24
+ - payload:
25
+ - `code`: <CODE>
26
+ - `command`: "clawcupid link"
27
+ - `channel`: "telegram" (또는 현재 채널명)
28
+ - `to`: **현재 대화의 to/chatId**
29
+
30
+ ### 1.2.1 Telegram DM에서 `to(chatId)` 얻는 규칙(MVP)
31
+ - Clawdbot이 사용자 메시지 앞에 붙이는 메타 라인(예: `... id:118032563 ...`)이 포함되는 경우가 많다.
32
+ - Telegram DM에서는 보통 `to(chatId) == 사용자 telegram id`이므로, 아래 우선순위로 추출한다:
33
+ 1) 사용자의 원문 메시지(메타 포함)에서 `id:<digits>` 패턴 추출 → `to`로 사용
34
+ 2) 추출 실패 시 사용자에게 “텔레그램 ID(숫자)”를 1회만 물어보고 저장(추후 자동화)
35
+
36
+ 실행은 스크립트 사용을 우선한다.
37
+ - `scripts/register_channel.mjs`
38
+
39
+ 예시(에이전트가 exec로 실행):
40
+ - raw 파싱 자동:
41
+ - `node skills/clawcupid-agent/scripts/register_channel.mjs --api <API> --raw "<사용자 메시지 원문>"`
42
+ - 명시 지정:
43
+ - `node skills/clawcupid-agent/scripts/register_channel.mjs --api <API> --code <CODE> --channel telegram --to <chatId>`
44
+
45
+ ### 1.3 사용자에게 응답
46
+ - 성공 시:
47
+ - `[ClawCupid] linked. 이제 “큐피드에게 매칭 서비스 시작한다고 얘기해줘” 라고 말해 주세요.`
48
+ - 실패 시:
49
+ - 코드 만료/오류를 짧게 설명하고, 웹에서 코드를 재발급받게 안내한다.
50
+
51
+ ## 2) 1단계: 프로필 수집(대화형)
52
+ 사용자가 “큐피드 시작/매칭 시작”류를 말하면, 다음을 순서대로 수집한다(최소):
53
+ - 불리는 이름
54
+ - regionCode (나라-도시)
55
+ - timezone (IANA TZ, 예: Asia/Seoul)
56
+ - 나이
57
+ - 나이 허용 범위(min/max)
58
+ - 불호(dealbreakers) 1~3개
59
+
60
+ 각 답변은 서버 API로 저장한다(엔드포인트는 서버 구현에 맞춰 호출).
61
+
62
+ ## 3) 2단계: 미션 루프(요약)
63
+ - Day1: 매칭 성사 즉시 시작 (Mission1 발송)
64
+ - Day2~Day7: 사용자 타임존 기준 10:00 또는 지정 시간
65
+ - 미션 1~5는 순차적으로 연속 입력 가능
66
+ - 피드백은 미션 단위로 동기화(양쪽 답변 완료 후 공개→피드백)
67
+ - 미완료 시 유예(리마인드)
68
+
69
+ ## 4) 7일 완료: 최후 수락(웹)
70
+ - 텔레그램에서 동의 버튼을 쓰지 않는다.
71
+ - 사용자에게 1회성 웹 링크를 제공하고, 웹에서 지난 로그를 전체 열람 후 동의한다.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Register current chat destination (to/chatId) to ClawCupid server for `clawcupid link <CODE>`.
4
+ *
5
+ * Designed to be called by an agent via exec.
6
+ *
7
+ * Usage (explicit):
8
+ * node register_channel.mjs --api http://127.0.0.1:3001 --code CC-xxx --channel telegram --to 118032563
9
+ *
10
+ * Usage (auto-parse from raw message text):
11
+ * node register_channel.mjs --api http://127.0.0.1:3001 --raw "... id:118032563 ... clawcupid link CC-xxx"
12
+ */
13
+
14
+ const args = process.argv.slice(2);
15
+ const get = (k) => {
16
+ const i = args.indexOf(k);
17
+ if (i === -1) return undefined;
18
+ return args[i + 1];
19
+ };
20
+
21
+ const api = get('--api') || process.env.CLAWCUPID_API || 'http://127.0.0.1:3001';
22
+ const raw = get('--raw');
23
+ let code = get('--code');
24
+ let channel = get('--channel') || 'telegram';
25
+ let to = get('--to');
26
+
27
+ if (raw) {
28
+ // Extract telegram id from meta: "id:118032563"
29
+ const mId = /\bid\s*:\s*(\d{5,})\b/.exec(raw);
30
+ if (mId) to = to ?? mId[1];
31
+
32
+ // Extract code from command: "clawcupid link CC-xxxx"
33
+ const mCode = /\bclawcupid\s+link\s+(CC-[A-Za-z0-9_-]{4,})\b/i.exec(raw);
34
+ if (mCode) code = code ?? mCode[1];
35
+
36
+ // Channel: best-effort
37
+ const mCh = /\bchannel\s*=\s*(telegram|whatsapp|signal|discord|slack|imessage)\b/i.exec(raw);
38
+ if (mCh) channel = channel ?? mCh[1].toLowerCase();
39
+ }
40
+
41
+ if (!code) {
42
+ console.error('Missing --code (or failed to parse from --raw)');
43
+ process.exit(2);
44
+ }
45
+
46
+ const payload = {
47
+ channel,
48
+ to,
49
+ code,
50
+ command: 'clawcupid link',
51
+ ts: Math.floor(Date.now() / 1000),
52
+ };
53
+
54
+ const res = await fetch(`${api}/api/callback/channel/register`, {
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify(payload),
58
+ });
59
+
60
+ const text = await res.text();
61
+ if (!res.ok) {
62
+ console.error('Failed:', res.status, text);
63
+ process.exit(1);
64
+ }
65
+
66
+ console.log('OK', text);