workq-mcp 0.1.7 → 0.1.8

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/lib/natural-git.mjs +122 -11
  2. package/package.json +1 -1
@@ -183,20 +183,58 @@ function stripTags(value) {
183
183
  return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
184
184
  }
185
185
 
186
+ function isProbablyCodeText(value) {
187
+ const text = value.trim();
188
+ if (!text) return true;
189
+ if (/[{}<>]|=>|\$\{/.test(text)) return true;
190
+ if (/[;]\s*$/.test(text)) return true;
191
+ if (/\b(?:on[A-Z]\w*|className|aria-label|text\.\w+|event\.|set[A-Z]\w*|function|const|let|return|await|async|JSON\.|api\(|onAction\(|document\.|querySelector)/.test(text)) return true;
192
+ if (/\w+\([^)]*\)/.test(text) && !/[가-힣]/.test(text)) return true;
193
+ if (/^[\w.$-]+\s*[=:]\s*[\w.$-]+$/.test(text)) return true;
194
+ return false;
195
+ }
196
+
197
+ function normalizeVisibleTextCandidate(value) {
198
+ const text = compact(stripTags(value).replace(/&nbsp;/g, " "), 100);
199
+ if (!text || /^[{}()[\];,.]+$/.test(text)) return "";
200
+ if (isProbablyCodeText(text)) return "";
201
+ return text;
202
+ }
203
+
204
+ function pushVisibleText(texts, value) {
205
+ const text = normalizeVisibleTextCandidate(value);
206
+ if (text) texts.push(text);
207
+ }
208
+
186
209
  function extractVisibleTexts(file) {
187
210
  const texts = [];
188
211
  const markupContent = file.content.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
189
212
  if (file.extension === ".html") {
190
- for (const match of file.content.matchAll(/<title[^>]*>([\s\S]*?)<\/title>/gi)) texts.push(stripTags(match[1]));
191
- for (const match of markupContent.matchAll(/>([^<>{}][^<>{}]{1,80})</g)) texts.push(stripTags(match[1]));
213
+ for (const match of file.content.matchAll(/<title[^>]*>([\s\S]*?)<\/title>/gi)) pushVisibleText(texts, match[1]);
214
+ }
215
+ if ([".html", ".tsx", ".jsx"].includes(file.extension)) {
216
+ for (const match of markupContent.matchAll(/>([^<>{}][^<>{}]{1,100})</g)) {
217
+ pushVisibleText(texts, match[1]);
218
+ }
192
219
  }
193
220
  for (const match of file.content.matchAll(/\b(?:textContent|innerText|placeholder|aria-label|title)\s*=\s*["'`]([^"'`]{1,100})["'`]/g)) {
194
- texts.push(stripTags(match[1]));
221
+ pushVisibleText(texts, match[1]);
195
222
  }
196
- for (const match of file.content.matchAll(/<button[^>]*>([\s\S]{1,160}?)<\/button>/gi)) {
197
- texts.push(stripTags(match[1]));
223
+ if (file.extension === ".html") {
224
+ for (const match of file.content.matchAll(/<button[^>]*>([\s\S]{1,160}?)<\/button>/gi)) {
225
+ pushVisibleText(texts, match[1]);
226
+ }
198
227
  }
199
- return unique(texts.map((item) => compact(item, 100)).filter((item) => item && !/^[{}()[\];,.]+$/.test(item)), 18);
228
+ return unique(texts, 18);
229
+ }
230
+
231
+ function visibleTextInitialStatements(file) {
232
+ const texts = extractVisibleTexts(file).slice(0, 8);
233
+ if (texts.length) return texts.map((item) => `"${item}" 문구가 보인다.`);
234
+ return [
235
+ "초기 화면에는 이 URL의 기본 콘텐츠와 조작 영역이 표시된다.",
236
+ "동적 문구는 원본 코드 조각이 아니라 실제 렌더링 결과를 기준으로 확인한다."
237
+ ];
200
238
  }
201
239
 
202
240
  function primaryVisibleTitle(file) {
@@ -906,6 +944,76 @@ function workQV2Regions() {
906
944
  ];
907
945
  }
908
946
 
947
+ function isWorkQDashboardPage(file) {
948
+ return (
949
+ file.path === "src/app/page.tsx" &&
950
+ /ProjectContextPanel/.test(file.content) &&
951
+ /ReportPanel/.test(file.content) &&
952
+ /TriagePanel/.test(file.content) &&
953
+ /AgentQueuePanel/.test(file.content) &&
954
+ /McpPanel/.test(file.content)
955
+ );
956
+ }
957
+
958
+ function workQDashboardRegions() {
959
+ return [
960
+ {
961
+ slug: "topbar-and-login",
962
+ title: "상단 상태 · 로그인 영역",
963
+ purpose: "사용자는 Work Q에 접속해 언어를 바꾸고, 소유자 세션으로 로그인한 뒤 현재 작업 상태를 확인한다.",
964
+ initial: ["상단에 Work Q 서비스 이름과 언어 전환, 새로고침 조작이 보인다.", "로그인 전에는 비밀번호 입력과 로그인 버튼이 보인다."],
965
+ actions: ["사용자는 표시 언어를 전환한다.", "사용자는 새로고침으로 최신 프로젝트와 작업 상태를 다시 불러온다.", "소유자는 비밀번호를 입력해 관리 화면에 들어간다."],
966
+ rules: ["로그인 전에는 프로젝트, 리포트, 큐 관리 영역을 보여주지 않는다.", "새로고침은 현재 선택된 프로젝트 컨텍스트를 유지해야 한다."],
967
+ verification: ["로그인 전에는 로그인 폼만 주요 조작으로 표시된다.", "언어 전환 후 화면 문구가 같은 의미로 바뀐다.", "새로고침 후 선택 프로젝트가 임의로 바뀌지 않는다."]
968
+ },
969
+ {
970
+ slug: "project-context",
971
+ title: "프로젝트 컨텍스트 영역",
972
+ purpose: "사용자는 여러 프로젝트와 레포 중 현재 Work Q가 판단하고 작업할 대상을 선택한다.",
973
+ initial: ["프로젝트 선택 목록과 새 프로젝트 입력 흐름이 보인다.", "프로젝트 키, 이름, 레포, 로컬 연결 상태를 확인할 수 있다."],
974
+ actions: ["사용자는 기존 프로젝트를 선택한다.", "사용자는 새 프로젝트 정보를 저장한다.", "사용자는 로컬 MCP가 연결한 레포 정보와 웹 프로젝트 정보가 맞는지 확인한다."],
975
+ rules: ["모든 리포트, 명세, 큐, 결정 필요 항목은 선택된 프로젝트 기준으로만 표시한다.", "프로젝트 키는 서로 다른 레포나 서비스를 섞지 않기 위한 안정적인 식별자다."],
976
+ verification: ["프로젝트를 바꾸면 리포트, 명세 지도, 큐 목록도 같은 프로젝트 기준으로 바뀐다.", "저장된 프로젝트 키와 MCP 연결 정보가 일치하지 않으면 사용자가 확인할 수 있어야 한다."]
977
+ },
978
+ {
979
+ slug: "report-intake",
980
+ title: "리포트 접수 영역",
981
+ purpose: "사용자는 버그, 개선, 기획문의, 개발문의를 올리고 필요한 맥락과 이미지를 첨부한다.",
982
+ initial: ["리포트 제목, 유형, URL, 실제 동작, 기대 동작, 재현 방법 입력 영역이 보인다.", "스크린샷 파일 선택 또는 붙여넣기 영역이 보인다."],
983
+ actions: ["사용자는 문제나 문의 내용을 작성한다.", "사용자는 스크린샷을 붙여넣거나 첨부한다.", "사용자는 리포트를 제출하고 분석 결과를 기다린다."],
984
+ rules: ["재현 방법은 비어 있으면 사용자가 어떤 행동을 했는지 판단하기 어렵기 때문에 입력 필요 상태로 남긴다.", "첨부 이미지는 리포트 판단 근거로 연결되지만 그 자체가 승인된 명세는 아니다."],
985
+ verification: ["리포트 제출 후 선택 프로젝트의 리포트 목록에 추가된다.", "스크린샷이 있는 리포트는 첨부 정보가 함께 남는다.", "필수 맥락이 부족하면 작업 큐로 바로 보내지 않고 입력 필요로 구분한다."]
986
+ },
987
+ {
988
+ slug: "triage-and-spec-map",
989
+ title: "판단 근거 · 기능명세 지도 영역",
990
+ purpose: "사용자는 리포트가 버그, 기획부재, 충돌, 개선 중 무엇인지와 그 판단 근거를 확인한다.",
991
+ initial: ["선택된 리포트의 분석 결과와 관련 기능 노드가 보인다.", "입력 필요, 결정 필요, 충돌 같은 상태가 카드나 칩으로 표시된다."],
992
+ actions: ["사용자는 판단 근거를 읽고 필요한 결정을 입력한다.", "사용자는 승인 가능한 항목을 Agent 큐로 보낸다.", "사용자는 부족한 정보를 보충해 다시 판단하게 한다."],
993
+ rules: ["승인된 기능명세와 다른 실제 동작이면 버그 후보로 본다.", "기대 동작은 있지만 승인된 명세가 없으면 기획부재 또는 결정 필요로 본다.", "서로 다른 승인 명세가 같은 조건에서 다른 결과를 요구하면 충돌로 본다."],
994
+ verification: ["각 판단 결과에는 어떤 명세 노드와 리포트 내용이 근거였는지 표시된다.", "결정 필요 항목은 사용자의 승인 또는 거절 전까지 자동 개발 큐로 넘어가지 않는다."]
995
+ },
996
+ {
997
+ slug: "project-memory-and-roadmap",
998
+ title: "프로젝트 기억 · 로드맵 영역",
999
+ purpose: "사용자는 현재 프로젝트의 기능명세, 알려진 충돌, 검증 상태, 가벼운 로드맵 메모를 확인한다.",
1000
+ initial: ["기능명세 지도와 프로젝트 메모, 로드맵 항목이 선택 프로젝트 기준으로 보인다."],
1001
+ actions: ["사용자는 로드맵 메모를 추가하거나 순서를 조정한다.", "사용자는 기능명세 카드의 상태를 확인한다."],
1002
+ rules: ["로드맵은 참고 메모이며 승인된 기능명세가 아니다.", "기능명세 지도는 리포트 분류와 Agent 작업의 기준으로 사용한다."],
1003
+ verification: ["로드맵 순서 변경이 같은 프로젝트 안에서 유지된다.", "명세 업데이트 후 기능명세 지도와 판단 근거가 같은 기준을 참조한다."]
1004
+ },
1005
+ {
1006
+ slug: "agent-queue-and-mcp",
1007
+ title: "Agent 큐 · MCP 연결 영역",
1008
+ purpose: "사용자는 데스크톱 Codex 또는 Claude가 가져갈 작업 큐와 MCP 연결 준비 상태를 확인한다.",
1009
+ initial: ["선택 프로젝트의 Agent 작업 목록과 상태가 보인다.", "MCP URL과 token 준비 상태가 보인다."],
1010
+ actions: ["로컬 Agent는 MCP로 다음 작업을 가져간다.", "작업이 끝난 Agent는 완료 결과와 검증 내용을 Work Q에 남긴다.", "사용자는 완료, 진행, 결정 필요 상태를 확인한다."],
1011
+ rules: ["큐 작업은 선택 프로젝트와 연결된 레포를 기준으로 처리해야 한다.", "Agent가 작업을 완료하면 완료 표시와 함께 명세 변경 또는 검증 근거를 남겨야 한다."],
1012
+ verification: ["다른 프로젝트의 큐가 섞여 보이지 않는다.", "MCP token이 없으면 로컬 Agent 연결이 준비되지 않은 상태로 보인다.", "완료된 작업은 큐 상태가 완료로 바뀐다."]
1013
+ }
1014
+ ];
1015
+ }
1016
+
909
1017
  function genericHtmlRegions(file) {
910
1018
  const title = primaryVisibleTitle(file) || "화면";
911
1019
  const hasOneToNineLoop = /\bfor\s*\(\s*let\s+\w+\s*=\s*1\s*;\s*\w+\s*<=\s*9\s*;/.test(file.content);
@@ -936,7 +1044,7 @@ function genericHtmlRegions(file) {
936
1044
  slug: "main-content",
937
1045
  title: `${title} 주요 화면 영역`,
938
1046
  purpose: "사용자는 이 화면에서 주요 콘텐츠와 조작 가능한 요소를 확인한다.",
939
- initial: extractVisibleTexts(file).slice(0, 8).map((item) => `"${item}" 문구가 보인다.`),
1047
+ initial: visibleTextInitialStatements(file),
940
1048
  actions: ["사용자는 화면에 표시된 버튼, 링크, 입력 요소를 조작한다."],
941
1049
  rules: ["화면에 보이는 문구와 조작 요소는 같은 URL 화면 명세 안에서 관리한다."],
942
1050
  verification: ["초기 화면의 주요 문구와 조작 요소가 표시된다."]
@@ -945,13 +1053,12 @@ function genericHtmlRegions(file) {
945
1053
  }
946
1054
 
947
1055
  function genericRouteRegions(file) {
948
- const texts = extractVisibleTexts(file);
949
1056
  return [
950
1057
  {
951
1058
  slug: "main-screen",
952
1059
  title: "주요 화면 영역",
953
1060
  purpose: "사용자는 이 URL에서 제공되는 핵심 화면을 본다.",
954
- initial: texts.slice(0, 8).map((item) => `"${item}" 문구가 보인다.`),
1061
+ initial: visibleTextInitialStatements(file),
955
1062
  actions: ["사용자는 화면에 표시된 버튼, 입력칸, 선택값을 조작한다."],
956
1063
  rules: ["같은 URL 패턴에 속한 상태 변화는 같은 화면 명세 안에서 관리한다."],
957
1064
  verification: ["URL 접속 시 주요 화면 문구와 조작 요소가 표시된다."]
@@ -960,15 +1067,19 @@ function genericRouteRegions(file) {
960
1067
  }
961
1068
 
962
1069
  function regionsForRoute(file, route) {
1070
+ if (isWorkQDashboardPage(file)) return workQDashboardRegions();
963
1071
  if (route.url_pattern === "/v2" && file.path === "src/app/v2/page.tsx") return workQV2Regions();
964
1072
  if (file.path === "index.html") return genericHtmlRegions(file);
965
1073
  return genericRouteRegions(file);
966
1074
  }
967
1075
 
968
1076
  function screenReadmeDoc(file, route, regions) {
1077
+ const isWorkQDashboard = isWorkQDashboardPage(file);
969
1078
  const isWorkQV2 = route.url_pattern === "/v2" && file.path === "src/app/v2/page.tsx";
970
- const title = isWorkQV2 ? "Work Q v2 대시보드 화면 명세" : route.title;
971
- const purpose = isWorkQV2
1079
+ const title = isWorkQDashboard ? "Work Q 대시보드 화면 명세" : isWorkQV2 ? "Work Q v2 대시보드 화면 명세" : route.title;
1080
+ const purpose = isWorkQDashboard
1081
+ ? "사용자는 Work Q에서 프로젝트를 선택하고, 리포트를 등록하고, 기능명세 판단 근거와 Agent 큐 상태를 확인한다."
1082
+ : isWorkQV2
972
1083
  ? "사용자는 Work Q v2에서 프로젝트와 레포를 연결하고, natural-spec repo를 편집하며, 리포트와 자연어 커밋을 Agent 작업 큐로 연결한다."
973
1084
  : `${route.url_pattern} URL에서 사용자가 보는 화면의 목적, 영역, 조작, 결과를 정의한다.`;
974
1085
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workq-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Work Q MCP stdio bridge and local repository connection CLI.",
5
5
  "type": "module",
6
6
  "bin": {