jutell 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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/assets/default-config.json +26 -0
  4. package/assets/local-admin/assets/index-CVml-p-C.css +1 -0
  5. package/assets/local-admin/assets/index-Gxd8X8ii.js +60 -0
  6. package/assets/local-admin/index.html +14 -0
  7. package/assets/local-admin-server.js +1391 -0
  8. package/assets/mcp-server/config/bridge-config.js +72 -0
  9. package/assets/mcp-server/index.js +42 -0
  10. package/assets/mcp-server/tools/bridge-tools.js +64 -0
  11. package/assets/mcp-server/tools/catalog.js +25 -0
  12. package/assets/mcp-server/tools/usage-counters.js +97 -0
  13. package/assets/skill/SKILL.md +135 -0
  14. package/assets/skill/references/explained-diff-format.md +117 -0
  15. package/assets/skill/references/feature-registry.md +29 -0
  16. package/assets/skill/references/glossary-ko.md +302 -0
  17. package/assets/skill/references/report-format.md +176 -0
  18. package/assets/skill/references/risk-level-guide.md +85 -0
  19. package/assets/templates/request-builder/BUG_REPORT_REQUEST.md +100 -0
  20. package/assets/templates/request-builder/CODE_REVIEW_REQUEST.md +93 -0
  21. package/assets/templates/request-builder/DESIGN_REQUEST.md +111 -0
  22. package/assets/templates/request-builder/FEATURE_REQUEST.md +93 -0
  23. package/assets/templates/request-builder/MANUAL_EDIT_GUIDE.md +95 -0
  24. package/assets/templates/request-builder/NEXT_AGENT_HANDOFF.md +106 -0
  25. package/assets/templates/request-builder/PROJECT_PLANNING_REQUEST.md +106 -0
  26. package/assets/templates/request-builder/README.md +48 -0
  27. package/assets/version.json +6 -0
  28. package/dist/cli.js +82 -0
  29. package/dist/commands/dashboard.js +81 -0
  30. package/dist/commands/default.js +103 -0
  31. package/dist/commands/lifecycle.js +166 -0
  32. package/dist/commands/migrate.js +159 -0
  33. package/dist/commands/provider.js +135 -0
  34. package/dist/commands/session/add-work.js +43 -0
  35. package/dist/commands/session/create-page.js +53 -0
  36. package/dist/commands/session/finish-session.js +28 -0
  37. package/dist/commands/session/index.js +76 -0
  38. package/dist/commands/session/move-page.js +37 -0
  39. package/dist/commands/session/new-session.js +24 -0
  40. package/dist/commands/session/operator-storage.js +126 -0
  41. package/dist/commands/session/prompt.js +77 -0
  42. package/dist/commands/session/storage-command.js +74 -0
  43. package/dist/commands/session/storage.js +212 -0
  44. package/dist/commands/session/types.js +1 -0
  45. package/dist/commands/status.js +208 -0
  46. package/dist/commands/upgrade.js +113 -0
  47. package/dist/commands/use.js +180 -0
  48. package/dist/compat.js +5 -0
  49. package/dist/config/managed.js +257 -0
  50. package/dist/config/paths.js +100 -0
  51. package/dist/index.js +4 -0
  52. package/dist/installer/agents.js +42 -0
  53. package/dist/installer/claude.js +160 -0
  54. package/dist/installer/config.js +45 -0
  55. package/dist/installer/opencode.js +237 -0
  56. package/dist/installer/providers.js +15 -0
  57. package/dist/installer/skill.js +94 -0
  58. package/dist/output/format.js +187 -0
  59. package/dist/process/mcpProbe.js +122 -0
  60. package/dist/process/system.js +34 -0
  61. package/dist/types.js +1 -0
  62. package/package.json +55 -0
@@ -0,0 +1,187 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ export function createIo() {
4
+ return {
5
+ write: (message) => console.log(message),
6
+ error: (message) => console.error(message),
7
+ ask: async (message, defaultYes = false) => {
8
+ const rl = readline.createInterface({ input, output });
9
+ try {
10
+ const answer = await rl.question(`${message} ${defaultYes ? '(Y/n)' : '(y/N)'} `);
11
+ return defaultYes ? !/^n(o)?$/i.test(answer.trim()) : /^y(es)?$/i.test(answer.trim());
12
+ }
13
+ finally {
14
+ rl.close();
15
+ }
16
+ },
17
+ choose: async (message, choices, defaultValue) => {
18
+ const rl = readline.createInterface({ input, output });
19
+ try {
20
+ const list = choices.map((choice, index) => ` ${index + 1}. ${choice.label}${choice.note ? ` — ${choice.note}` : ''}`).join('\n');
21
+ output.write(`${message}\n${list}\n`);
22
+ const defaultLabel = defaultValue ? choices.find((choice) => choice.value === defaultValue)?.label : undefined;
23
+ while (true) {
24
+ const answer = await rl.question(`선택 (번호 입력${defaultLabel ? `, Enter: ${defaultLabel}` : ''}) > `);
25
+ const trimmed = answer.trim();
26
+ if (!trimmed) {
27
+ if (defaultLabel)
28
+ return defaultValue;
29
+ output.write('보기 중 하나의 번호를 입력하세요.\n');
30
+ continue;
31
+ }
32
+ const number = Number(trimmed);
33
+ if (Number.isInteger(number) && number >= 1 && number <= choices.length)
34
+ return choices[number - 1].value;
35
+ output.write('보기 중 하나의 번호를 입력하세요.\n');
36
+ }
37
+ }
38
+ finally {
39
+ rl.close();
40
+ }
41
+ },
42
+ };
43
+ }
44
+ export function parseOptions(args) {
45
+ let command = 'dashboard';
46
+ let defaultInvocation = true;
47
+ let index = 0;
48
+ const extraArgs = [];
49
+ if (args[0] && !args[0].startsWith('-')) {
50
+ command = args[0];
51
+ defaultInvocation = false;
52
+ index = 1;
53
+ }
54
+ const options = { scope: 'project', yes: false, activateMcp: false, oneCommand: false, statusOnly: false, json: false, verbose: false, openBrowser: true, fix: false, skillOnly: false, mcpOnly: false, disableSkill: false, disableMcp: false, disableAll: false, keepData: false, removeData: false, clean: false };
55
+ for (; index < args.length; index += 1) {
56
+ const arg = args[index];
57
+ if (arg === '--project')
58
+ options.scope = 'project';
59
+ else if (arg === '--global')
60
+ options.scope = 'global';
61
+ else if (arg === '--yes' || arg === '-y')
62
+ options.yes = true;
63
+ else if (arg === '--json')
64
+ options.json = true;
65
+ else if (arg === '--status-only')
66
+ options.statusOnly = true;
67
+ else if (arg === '--verbose')
68
+ options.verbose = true;
69
+ else if (arg === '--no-open')
70
+ options.openBrowser = false;
71
+ else if (arg === '--fix')
72
+ options.fix = true;
73
+ else if (arg === '--skill-only')
74
+ options.skillOnly = true;
75
+ else if (arg === '--mcp-only')
76
+ options.mcpOnly = true;
77
+ else if (arg === '--skill')
78
+ options.disableSkill = true;
79
+ else if (arg === '--mcp')
80
+ options.disableMcp = true;
81
+ else if (arg === '--all')
82
+ options.disableAll = true;
83
+ else if (arg === '--keep-data')
84
+ options.keepData = true;
85
+ else if (arg === '--remove-data')
86
+ options.removeData = true;
87
+ else if (arg === '--clean' || arg === '--remove-legacy')
88
+ options.clean = true;
89
+ else if (arg === '--profile') {
90
+ const value = args[index + 1];
91
+ if (!value)
92
+ throw new Error('--profile 뒤에 Profile 이름이 필요합니다.');
93
+ options.profile = value;
94
+ index += 1;
95
+ }
96
+ else if (arg === '--page') {
97
+ const value = args[index + 1];
98
+ if (!value || !/^\d+$/.test(value))
99
+ throw new Error('--page 뒤에 Page 번호가 필요합니다.');
100
+ options.page = Number(value);
101
+ index += 1;
102
+ }
103
+ else if (arg === '--agent') {
104
+ const value = args[index + 1];
105
+ if (!value)
106
+ throw new Error('--agent 뒤에 Agent 이름이 필요합니다.');
107
+ options.agent = value;
108
+ index += 1;
109
+ }
110
+ else if (arg === '--role') {
111
+ const value = args[index + 1];
112
+ if (!value)
113
+ throw new Error('--role 뒤에 역할이 필요합니다.');
114
+ options.role = value;
115
+ index += 1;
116
+ }
117
+ else if (arg === '--title') {
118
+ const value = args[index + 1];
119
+ if (!value)
120
+ throw new Error('--title 뒤에 Page 제목이 필요합니다.');
121
+ options.title = value;
122
+ index += 1;
123
+ }
124
+ else if (arg === '--help' || arg === '-h')
125
+ command = 'help';
126
+ else if (arg.startsWith('-'))
127
+ throw new Error(`알 수 없는 옵션입니다: ${arg}`);
128
+ else
129
+ extraArgs.push(arg);
130
+ }
131
+ if (options.skillOnly && options.mcpOnly)
132
+ throw new Error('--skill-only와 --mcp-only를 동시에 사용할 수 없습니다.');
133
+ if (options.keepData && options.removeData)
134
+ throw new Error('--keep-data와 --remove-data를 동시에 사용할 수 없습니다.');
135
+ if (options.disableAll) {
136
+ options.disableSkill = true;
137
+ options.disableMcp = true;
138
+ }
139
+ return { command, options, defaultInvocation, extraArgs };
140
+ }
141
+ export function scopeLabel(scope) { return scope === 'global' ? '사용자 전역' : '현재 프로젝트'; }
142
+ export function printHelp(io) {
143
+ io.write(`JuTell CLI 0.3.0
144
+
145
+ 시작할 때는 jutell만 입력하면 됩니다.
146
+ 처음 연결하면 안내에 따라 AI Agent와 보고 방식을 고르고
147
+ 연결을 준비한 뒤 관리자 화면을 엽니다.
148
+
149
+ 자주 쓰는 명령
150
+
151
+ jutell 처음 시작: 설치·연결·관리자 화면을 준비합니다.
152
+ jutell use codex Codex에 연결합니다 (권장).
153
+ jutell use opencode OpenCode에 연결합니다.
154
+ jutell use claude Claude Code에 연결합니다 (베타).
155
+ jutell status 현재 연결 상태를 확인합니다.
156
+ jutell doctor 문제가 있는지 점검합니다.
157
+ jutell on 연결을 켭니다.
158
+ jutell off 연결을 끕니다.
159
+
160
+ 연결 후 새 대화를 열면 JuTell이 자동으로 적용됩니다.
161
+ JuTell은 AI Agent를 대신 실행하지 않고 연결과 보고만 도와줍니다.
162
+
163
+ 고급 명령 (보통 사용할 필요가 없습니다)
164
+
165
+ jutell dashboard 관리자 화면만 엽니다.
166
+ jutell setup 설치를 다시 진행합니다.
167
+ jutell enable 연결을 켭니다 (on과 같음).
168
+ jutell disable 연결을 끕니다 (off와 같음).
169
+ jutell provider Agent 연결 상태를 자세히 봅니다.
170
+ jutell connect 연결만 추가합니다.
171
+ jutell disconnect 해당 연결만 끕니다.
172
+ jutell switch 기본 Agent를 전환합니다.
173
+ jutell uninstall 설치를 제거합니다.
174
+ jutell upgrade 설치된 Skill/설정/MCP를 최신으로 새로고침합니다.
175
+ jutell migrate 레거시 beginner_bridge → jutell 로 안전하게 옮깁니다.
176
+ jutell migrate --clean 레거시 정리를 수행합니다 (canonical 확인 후).
177
+
178
+ 하루 작업 기록
179
+
180
+ jutell session 오늘 기록 상태를 봅니다.
181
+ jutell session help 하루 기록의 하위 명령을 보여줍니다.
182
+
183
+ 이전 별칭: beginner-bridge
184
+
185
+ 실제 배포 전에는 로컬 패키지 검증만 지원합니다. 업데이트는 다음 명령을 사용하세요.
186
+ npm update -g jutell`);
187
+ }
@@ -0,0 +1,122 @@
1
+ import { spawn } from 'node:child_process';
2
+ const PROTOCOL_VERSION = '2025-03-26';
3
+ const TIMEOUT_MS = 15000;
4
+ const SHUTDOWN_GRACE_MS = 1500;
5
+ const INITIALIZE = JSON.stringify({
6
+ jsonrpc: '2.0',
7
+ id: 1,
8
+ method: 'initialize',
9
+ params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'jutell-doctor', version: '0.3.0' } },
10
+ });
11
+ const INITIALIZED = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' });
12
+ const TOOLS_LIST = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
13
+ function tryParse(line) {
14
+ try {
15
+ return JSON.parse(line.trim());
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ export function probeMcpServer(entry) {
22
+ return new Promise((resolve) => {
23
+ const child = spawn(process.execPath, [entry], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
24
+ let buffer = '';
25
+ let stderrText = '';
26
+ let settled;
27
+ let initializedSent = false;
28
+ let toolsListSent = false;
29
+ let receivedInitialize = false;
30
+ let receivedTools = false;
31
+ let serverName = '';
32
+ const finish = (result) => {
33
+ if (settled)
34
+ return;
35
+ settled = result;
36
+ clearTimeout(timeout);
37
+ try {
38
+ child.stdin.end();
39
+ }
40
+ catch { /* stdin already closed */ }
41
+ try {
42
+ child.kill();
43
+ }
44
+ catch { /* already exited */ }
45
+ const fallback = setTimeout(() => resolve(result), SHUTDOWN_GRACE_MS);
46
+ child.once('exit', () => { clearTimeout(fallback); resolve(result); });
47
+ };
48
+ const timeout = setTimeout(() => {
49
+ const phase = !receivedInitialize ? 'initialize 응답' : !receivedTools ? 'tools/list 응답' : '종료';
50
+ const detail = stderrText.trim().slice(0, 200);
51
+ finish({ ok: false, toolCount: 0, serverName, error: `${phase} 시간 초과${detail ? `: ${detail}` : ''}` });
52
+ }, TIMEOUT_MS);
53
+ const handleMessage = (message) => {
54
+ if (message.id === 1) {
55
+ receivedInitialize = true;
56
+ if (message.error) {
57
+ finish({ ok: false, toolCount: 0, serverName, error: `initialize 실패: ${message.error.message ?? '알 수 없는 오류'}` });
58
+ return;
59
+ }
60
+ const name = message.result?.serverInfo?.name;
61
+ if (typeof name === 'string')
62
+ serverName = name;
63
+ if (!initializedSent) {
64
+ initializedSent = true;
65
+ try {
66
+ child.stdin.write(`${INITIALIZED}\n`);
67
+ }
68
+ catch { /* closed */ }
69
+ }
70
+ if (!toolsListSent) {
71
+ toolsListSent = true;
72
+ try {
73
+ child.stdin.write(`${TOOLS_LIST}\n`);
74
+ }
75
+ catch { /* closed */ }
76
+ }
77
+ return;
78
+ }
79
+ if (message.id === 2) {
80
+ receivedTools = true;
81
+ if (message.error) {
82
+ finish({ ok: false, toolCount: 0, serverName, error: `tools/list 실패: ${message.error.message ?? '알 수 없는 오류'}` });
83
+ return;
84
+ }
85
+ const tools = message.result?.tools ?? [];
86
+ finish({ ok: true, toolCount: tools.length, serverName });
87
+ return;
88
+ }
89
+ };
90
+ child.stdout.on('data', (chunk) => {
91
+ buffer += String(chunk);
92
+ let newlineIndex = buffer.indexOf('\n');
93
+ while (newlineIndex !== -1) {
94
+ const line = buffer.slice(0, newlineIndex);
95
+ buffer = buffer.slice(newlineIndex + 1);
96
+ if (line.trim()) {
97
+ const message = tryParse(line);
98
+ if (message)
99
+ handleMessage(message);
100
+ }
101
+ newlineIndex = buffer.indexOf('\n');
102
+ }
103
+ });
104
+ child.stderr.on('data', (chunk) => { stderrText += String(chunk); });
105
+ child.once('error', (error) => {
106
+ finish({ ok: false, toolCount: 0, serverName, error: `프로세스 시작 실패: ${error.message}` });
107
+ });
108
+ child.once('exit', (code) => {
109
+ if (settled)
110
+ return;
111
+ const detail = stderrText.trim().slice(0, 200);
112
+ const phase = !receivedInitialize ? 'initialize 전' : !receivedTools ? 'tools/list 전' : '응답 후';
113
+ finish({ ok: false, toolCount: 0, serverName, error: `프로세스가 ${phase} 종료됨(code ${code})${detail ? `: ${detail}` : ''}` });
114
+ });
115
+ try {
116
+ child.stdin.write(`${INITIALIZE}\n`);
117
+ }
118
+ catch (error) {
119
+ finish({ ok: false, toolCount: 0, serverName, error: `initialize 전송 실패: ${error instanceof Error ? error.message : String(error)}` });
120
+ }
121
+ });
122
+ }
@@ -0,0 +1,34 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import os from 'node:os';
3
+ export function codexDetected() {
4
+ const result = spawnSync('codex', ['--version'], { stdio: 'ignore', windowsHide: true, shell: process.platform === 'win32' || undefined });
5
+ return result.status === 0 && !result.error;
6
+ }
7
+ export function claudeDetected() {
8
+ const result = spawnSync('claude', ['--version'], { stdio: 'ignore', windowsHide: true, shell: process.platform === 'win32' || undefined });
9
+ return result.status === 0 && !result.error;
10
+ }
11
+ export function nodeMajorVersion() {
12
+ const match = process.versions.node.match(/^(\d+)/);
13
+ return match ? Number(match[1]) : 0;
14
+ }
15
+ export function operatingSystem() {
16
+ if (process.platform === 'win32')
17
+ return 'Windows';
18
+ if (process.platform === 'darwin')
19
+ return 'macOS';
20
+ if (process.platform === 'linux')
21
+ return 'Linux';
22
+ return os.platform();
23
+ }
24
+ export async function openBrowser(url, io) {
25
+ try {
26
+ const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
27
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
28
+ const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
29
+ child.unref();
30
+ }
31
+ catch {
32
+ io.write('기본 브라우저를 열지 못했습니다. 위 URL을 직접 열어주세요.');
33
+ }
34
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "jutell",
3
+ "version": "0.3.0",
4
+ "description": "JuTell by Ju0 — non-developer harness for AI coding agents (Skill, MCP, local dashboard)",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ju0o/jutell.git"
9
+ },
10
+ "homepage": "https://github.com/ju0o/jutell#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/ju0o/jutell/issues"
13
+ },
14
+ "keywords": [
15
+ "jutell",
16
+ "ai",
17
+ "agent",
18
+ "harness",
19
+ "mcp",
20
+ "skill",
21
+ "codex",
22
+ "opencode",
23
+ "claude-code"
24
+ ],
25
+ "author": "Ju0",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "type": "module",
30
+ "bin": {
31
+ "jutell": "dist/index.js",
32
+ "beginner-bridge": "dist/compat.js"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "assets",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json && node scripts/build-assets.mjs",
42
+ "test": "vitest run",
43
+ "prepack": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.30.0",
47
+ "zod": "^4.4.3"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.10.2",
51
+ "esbuild": "^0.24.2",
52
+ "typescript": "^5.7.2",
53
+ "vitest": "^2.1.9"
54
+ }
55
+ }