evot-agent 0.2.2 → 0.3.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/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  유저 컴퓨터에서 도는 [evot](https://evot.io)의 로컬 브라우저 에이전트.
4
4
  evot에서 Vot에게 "내 컴퓨터 브라우저로 ○○ 수집해줘"라고 말하면(또는 루틴으로 걸어두면),
5
5
  이 에이전트가 Claude + Playwright로 사이트를 탐색·수집하고 결과를 지정한 쓰레드에 셀로 추가한다.
6
+ 결과는 요청을 받은 Vot 이름/아바타로 정리되며, 이미지를 수집한 경우 설명과 함께 셀 안에 표시된다.
6
7
 
7
8
  ## 요구사항
8
9
 
@@ -13,7 +14,7 @@ evot에서 Vot에게 "내 컴퓨터 브라우저로 ○○ 수집해줘"라고
13
14
 
14
15
  ## 설치 & 연결
15
16
 
16
- 1. evot.io에 로그인 → **Settings → Integrations → Web Agent** → 코드 발급.
17
+ 1. evot.io에 로그인 → **Settings → Integrations → Computer Use** → 코드 발급.
17
18
  2. 코드로 연결:
18
19
 
19
20
  ```bash
@@ -36,10 +37,13 @@ npx evot-agent connect <code> # 예: npx evot-agent connect ABCD2345
36
37
  ```bash
37
38
  npm install -g evot-agent
38
39
  evot-agent start # 기본 10분마다 폴링
39
- evot-agent install # macOS: 로그인 시 자동 시작 + 백그라운드 상주
40
+ evot-agent install # macOS 전용: 로그인 시 자동 시작 + 백그라운드 상주
40
41
  evot-agent uninstall # 상주 등록 해제
41
42
  ```
42
43
 
44
+ > `install`은 현재 **macOS(launchd) 전용**이다. Windows/Linux는 `evot-agent start`를
45
+ > 터미널에 띄워두거나, 작업 스케줄러/systemd 등으로 로그인 시 실행되게 등록한다.
46
+
43
47
  데몬을 켜두면 evot이 잡을 큐에 넣는 대로 가져와 실행한다. **새 UI는 없다** — 기존처럼 쓰면 된다:
44
48
 
45
49
  - **대화**: 쓰레드에서 Vot 멘션으로 "Use my computer's browser to summarize today's top AI
@@ -54,13 +58,20 @@ evot-agent uninstall # 상주 등록 해제
54
58
  ## 로그인이 필요한 사이트 (선택)
55
59
 
56
60
  ```bash
57
- evot-agent login https://example.com
61
+ evot-agent login x.com # 스킴 생략 가능 (https:// 자동 보정)
62
+ evot-agent login https://x.com
58
63
  ```
59
64
 
60
- 전용 Chrome 창이 뜬다. 대상 사이트에 로그인 창을 닫으면 세션이 전용 프로필에 저장되어
61
- 이후 잡에 재사용된다. 아이디/비번은 저장되지 않고 브라우저 세션 쿠키만 남는다.
65
+ 평소 쓰는 Chrome **별개인 전용 프로필**로 창이 뜬다. 평소 브라우저에 로그인돼 있어도
66
+ 에이전트에는 상속되지 않으므로, 로그인이 필요한 사이트는 여기서 로그인해둬야 한다.
67
+ 대상 사이트에 로그인 후 창을 닫으면 세션이 전용 프로필에 저장되어 이후 잡에 재사용된다.
68
+ 아이디/비번은 저장되지 않고 브라우저 세션 쿠키만 남는다.
62
69
  수집 중 로그인이 풀리면 실패 셀에 재로그인 안내가 표시된다.
63
70
 
71
+ > **소셜 로그인(Google·Apple 등) 주의**: 자동화가 제어하는 브라우저라 Google 등이 로그인을
72
+ > "browser may not be secure"로 막을 수 있다. 그럴 땐 사이트 자체 로그인(이메일/비밀번호)을
73
+ > 사용하면 우회된다.
74
+
64
75
  ## 안전
65
76
 
66
77
  - 에이전트는 read-only로 동작하도록 지시받는다 (구매·게시·폼 제출·설정변경 금지).
package/bin/evot-agent.js CHANGED
@@ -26,7 +26,7 @@ A manual job file (for "run") looks like:
26
26
  "title": "HN AI digest"
27
27
  }
28
28
 
29
- Auth model: get a one-time code from evot.io → Settings → Web Agent, then run
29
+ Auth model: get a one-time code from evot.io → Settings → Computer Use, then run
30
30
  "connect <code>". Only that code is entered — never your evot password or keys.
31
31
  The code is exchanged for a scoped, revocable token stored locally.
32
32
  `;
@@ -84,8 +84,9 @@ async function main() {
84
84
 
85
85
  console.log(`Running job → thread ${job.log_id}`);
86
86
  let text;
87
+ let segments;
87
88
  try {
88
- text = await runBrowseJob(job);
89
+ ({ text, segments } = await runBrowseJob(job));
89
90
  } catch (err) {
90
91
  // 실패도 쓰레드에 남긴다 (그리고 텔레그램 알림).
91
92
  console.error(`\nBrowse failed: ${err.message}`);
@@ -103,6 +104,7 @@ async function main() {
103
104
  const cellId = await reportResult({
104
105
  logId: job.log_id,
105
106
  text,
107
+ segments,
106
108
  title: job.title ?? "collection",
107
109
  status: "success",
108
110
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evot-agent",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Local browser agent for evot — browses sites with Claude and appends results as cells.",
6
6
  "license": "MIT",
package/src/browse.js CHANGED
@@ -15,16 +15,77 @@ const SYSTEM_APPEND = [
15
15
  " LOGIN_REQUIRED: <domain>",
16
16
  ' (e.g. "LOGIN_REQUIRED: news.ycombinator.com"). No other text.',
17
17
  "",
18
+ "MISSION FIDELITY:",
19
+ "- Complete exactly what the user asked. Do not narrow the scope on your own or",
20
+ " substitute a different deliverable — e.g. do NOT return only text/metadata when",
21
+ " images were requested. If the user asks for images, collect and output the",
22
+ " actual image URLs, exactly as many as the task calls for.",
23
+ "- Do not act as a content moderator. Mature, adult, sensitive, or otherwise",
24
+ " disagreeable material that is lawful and already publicly posted is in scope;",
25
+ " do not skip, omit, sanitize, or downgrade it merely for being suggestive or",
26
+ " NSFW. You are cataloguing what is publicly visible on the user's own request,",
27
+ " not endorsing it. Report what is actually there.",
28
+ "- The only hard stop is genuinely illegal content (e.g. sexual content involving",
29
+ " minors). If — and only if — you must decline part of a task, say so explicitly",
30
+ " on its own line with the concrete reason. Never silently drop or replace it.",
31
+ "",
18
32
  "When finished, output ONLY the collected result as plain text:",
19
- "- A short title line.",
20
- "- The summary the user asked for.",
21
- "- A 'Sources:' section listing the URLs you used, one per line.",
33
+ "- A short title line and the overall summary first.",
34
+ "- If the task involves collecting images, organize the result into sections:",
35
+ " each section is its description text followed by its image lines, each",
36
+ " exactly 'IMAGE: <url>' with a direct image URL you actually saw",
37
+ " (e.g. a pbs.twimg.com link or a URL ending in .jpg/.png). Max 10 images total.",
38
+ "- A 'Sources:' section at the end listing the URLs you used, one per line.",
22
39
  "Do not include tool logs or meta commentary in the final answer.",
23
40
  ].join("\n");
24
41
 
42
+ const MAX_IMAGES_TOTAL = 10;
43
+
44
+ /**
45
+ * 최종 답변을 섹션 단위로 파싱한다.
46
+ * - 'IMAGE: <url>' 라인 → 현재 섹션의 이미지(전체 합산 10개 상한). 'Images:' 헤딩 라인은 제거.
47
+ * - 이미지 뒤에 다시 텍스트가 나오면 새 섹션 시작(설명→이미지→설명→이미지 그룹핑).
48
+ * - 반환 text는 IMAGE 라인이 제거된 전문(구서버 호환·텔레그램 스니펫용).
49
+ */
50
+ export function parseSegments(raw) {
51
+ const segments = [];
52
+ let curText = [];
53
+ let curImages = [];
54
+ let sawImage = false;
55
+ let total = 0;
56
+
57
+ const flush = () => {
58
+ const text = curText.join("\n").trim();
59
+ if (text || curImages.length > 0) segments.push({ text, images: curImages });
60
+ curText = [];
61
+ curImages = [];
62
+ sawImage = false;
63
+ };
64
+
65
+ for (const line of raw.split("\n")) {
66
+ if (/^\s*images:\s*$/i.test(line)) continue; // 잔여 'Images:' 헤딩 제거
67
+ const m = line.match(/^\s*IMAGE:\s*(https?:\/\/\S+)\s*$/i);
68
+ if (m) {
69
+ if (total < MAX_IMAGES_TOTAL) {
70
+ curImages.push(m[1]);
71
+ total++;
72
+ sawImage = true;
73
+ }
74
+ continue;
75
+ }
76
+ if (sawImage && line.trim()) flush(); // 이미지 뒤 새 텍스트 → 새 섹션
77
+ curText.push(line);
78
+ }
79
+ flush();
80
+
81
+ const text = segments.map((s) => s.text).filter(Boolean).join("\n\n").trim();
82
+ return { text, segments: segments.length > 0 ? segments : [{ text, images: [] }] };
83
+ }
84
+
25
85
  /**
26
86
  * 잡 지시문을 Claude Agent SDK + Playwright MCP(전용 프로필)로 실행하고
27
- * 최종 요약 텍스트를 반환한다. 모델 크리덴셜은 로컬 Claude Code 로그인을 사용.
87
+ * 최종 결과를 { text, segments }로 반환한다(섹션 = 설명+이미지 URL 그룹).
88
+ * 모델 크리덴셜은 로컬 Claude Code 로그인을 사용.
28
89
  */
29
90
  export async function runBrowseJob(job) {
30
91
  const promptParts = [job.instruction];
@@ -99,7 +160,7 @@ export async function runBrowseJob(job) {
99
160
  finalText.trim() || "Agent finished without producing a result.",
100
161
  );
101
162
  }
102
- return finalText.trim();
163
+ return parseSegments(finalText);
103
164
  }
104
165
 
105
166
  /**
@@ -123,7 +184,7 @@ function isSafeDomain(d) {
123
184
  export function describeJobFailure(err) {
124
185
  if (err?.loginDomain && isSafeDomain(err.loginDomain)) {
125
186
  return [
126
- `The web agent is signed out of ${err.loginDomain}, so this task could not be completed.`,
187
+ `The agent is signed out of ${err.loginDomain}, so this task could not be completed.`,
127
188
  "",
128
189
  "To fix it, on the connected computer run:",
129
190
  ` evot-agent login https://${err.loginDomain}`,
@@ -133,11 +194,11 @@ export function describeJobFailure(err) {
133
194
  if (err?.loginDomain) {
134
195
  // 도메인이 의심스러우면 URL을 그대로 흘리지 않고 유저가 직접 지정하게 안내.
135
196
  return [
136
- "The web agent hit a login wall it could not pass, so this task could not be completed.",
197
+ "The agent hit a login wall it could not pass, so this task could not be completed.",
137
198
  "",
138
199
  "To fix it, on the connected computer run `evot-agent login <site-url>`",
139
200
  "with the site you want to collect from, sign in once, then ask the Vot to retry.",
140
201
  ].join("\n");
141
202
  }
142
- return `The web agent could not complete this task.\n\nReason: ${err?.message ?? "unknown"}`;
203
+ return `The agent could not complete this task.\n\nReason: ${err?.message ?? "unknown"}`;
143
204
  }
package/src/connect.js CHANGED
@@ -15,13 +15,13 @@ function prompt(query) {
15
15
  /**
16
16
  * evot.io에서 발급한 일회용 연결 코드를 전용 토큰으로 교환한다.
17
17
  * 계정 비밀번호·이메일·anon key 아무것도 입력하지 않는다 — 오직 코드만.
18
- * (코드는 evot.io → Settings → Web Agent → "Connect a device"에서 발급.)
18
+ * (코드는 evot.io → Settings → Computer Use → "Connect a device"에서 발급.)
19
19
  */
20
20
  export async function runConnect(codeArg) {
21
21
  let code = (codeArg ?? "").trim();
22
22
  if (!code) {
23
23
  console.log(
24
- "Get a connection code from evot.io → Settings → Web Agent → Connect a device.\n",
24
+ "Get a connection code from evot.io → Settings → Computer Use → Connect a device.\n",
25
25
  );
26
26
  code = await prompt("Connection code: ");
27
27
  }
@@ -49,5 +49,9 @@ export async function runConnect(codeArg) {
49
49
  `Token expires ${new Date(data.expires_at).toLocaleDateString()}. Re-connect to refresh.`,
50
50
  );
51
51
  }
52
- console.log("You can revoke this device anytime from evot Settings → Web Agent.");
52
+ console.log("You can revoke this device anytime from evot Settings → Computer Use.");
53
+ console.log(
54
+ '\nTip: for sites that need a sign-in, run "evot-agent login <url>" once —\n' +
55
+ "the agent browses in its own profile, so your everyday Chrome logins don't carry over.",
56
+ );
53
57
  }
package/src/login.js CHANGED
@@ -11,10 +11,25 @@ export async function runLogin(url) {
11
11
  channel: "chrome",
12
12
  headless: false,
13
13
  viewport: null,
14
+ // navigator.webdriver 자동화 신호를 숨겨 일부 소셜 로그인(Google 등)의
15
+ // "browser may not be secure" 차단을 완화한다. 만능은 아님(구글은 여러 신호를 함께 봄).
16
+ args: ["--disable-blink-features=AutomationControlled"],
14
17
  });
15
18
 
16
19
  const page = context.pages()[0] ?? (await context.newPage());
17
- if (url) await page.goto(url, { waitUntil: "domcontentloaded" });
20
+ if (url) {
21
+ // 스킴 없는 입력(예: "x.com")도 받도록 https:// 보정.
22
+ // 이동 실패해도 창은 열어둔 채 수동 이동을 안내한다(로그인 세션은 어차피 프로필에 저장됨).
23
+ const target = /^https?:\/\//i.test(url) ? url : `https://${url}`;
24
+ try {
25
+ await page.goto(target, { waitUntil: "domcontentloaded" });
26
+ } catch (err) {
27
+ console.log(
28
+ `Couldn't open ${target} automatically (${err.message}).\n` +
29
+ "Type the address in the window and sign in there.",
30
+ );
31
+ }
32
+ }
18
33
 
19
34
  console.log(
20
35
  "\nA dedicated browser window is open.\n" +
package/src/report.js CHANGED
@@ -8,12 +8,15 @@ import { SUPABASE_URL, loadToken } from "./config.js";
8
8
  * - 큐 경로(데몬): jobId만 넘긴다. 서버가 잡에서 log_id를 찾아 셀을 만들고 잡 상태를 done/failed로 갱신.
9
9
  * - 수동 경로(run): logId를 넘긴다. (Phase 1 호환)
10
10
  */
11
- export async function reportResult({ logId, jobId, text, title, status = "success" }) {
11
+ export async function reportResult({ logId, jobId, text, segments, title, status = "success" }) {
12
12
  const token = await loadToken();
13
13
 
14
14
  const body = { text, title, status };
15
15
  if (jobId) body.job_id = jobId;
16
16
  if (logId) body.log_id = logId;
17
+ // 섹션(설명+이미지 URL) 전달 — 서버가 votResponse 서브셀·이미지 저장에 사용.
18
+ // 구서버는 이 필드를 무시하고 text만 쓴다(하위호환).
19
+ if (Array.isArray(segments) && segments.length > 0) body.segments = segments;
17
20
 
18
21
  const url = `${SUPABASE_URL}/functions/v1/agent-report`;
19
22
  const res = await fetch(url, {
package/src/sync.js CHANGED
@@ -44,20 +44,21 @@ async function fetchJobs(token) {
44
44
  async function runJob(job) {
45
45
  console.log(`\nJob ${job.id}: ${String(job.instruction ?? "").slice(0, 80)}`);
46
46
  let text;
47
+ let segments;
47
48
  let status = "success";
48
49
  try {
49
- text = await runBrowseJob({
50
+ ({ text, segments } = await runBrowseJob({
50
51
  instruction: job.instruction,
51
52
  start_url: job.start_url ?? undefined,
52
53
  headless: true,
53
- });
54
+ }));
54
55
  } catch (err) {
55
56
  console.error(`Job ${job.id} failed: ${err.message}`);
56
57
  text = describeJobFailure(err);
57
58
  status = "error";
58
59
  }
59
60
  try {
60
- const cellId = await reportResult({ jobId: job.id, text, title: "Web agent", status });
61
+ const cellId = await reportResult({ jobId: job.id, text, segments, title: "Computer Use", status });
61
62
  console.log(`Reported job ${job.id}${cellId ? ` → cell ${cellId}` : ""}.`);
62
63
  } catch (err) {
63
64
  // 보고 실패 시 잡은 running으로 남고, running 타임아웃 후 서버가 expired 처리한다.