jutell 2.0.0 → 2.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/README.md +169 -22
- package/assets/mcp-server/index.js +1 -1
- package/assets/skill/SKILL.md +1 -1
- package/assets/version.json +1 -1
- package/dist/cli.js +5 -0
- package/dist/commands/migrate.js +25 -6
- package/dist/commands/status.js +11 -4
- package/dist/commands/use.js +7 -3
- package/dist/config/managed.js +24 -3
- package/dist/installer/agents.js +33 -6
- package/dist/installer/claude.js +16 -0
- package/dist/output/format.js +1 -1
- package/dist/output/funding.js +38 -0
- package/dist/process/mcpProbe.js +1 -1
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -1,39 +1,186 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JuTell
|
|
2
2
|
|
|
3
|
-
**Tell your
|
|
3
|
+
**Tell your agent what you mean. Understand what it did.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/jutell)
|
|
6
|
+
[](https://github.com/ju0o/jutell/blob/main/LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
For people who can tell a coding agent what they want, but don't want to read code or logs just to
|
|
13
|
+
know whether it actually worked.
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
npm install -g jutell
|
|
9
17
|
jutell
|
|
10
18
|
```
|
|
11
19
|
|
|
12
|
-
`jutell` finds the coding agents
|
|
20
|
+
That's the whole setup. `jutell` finds the coding agents already installed on your machine, asks
|
|
21
|
+
for one approval, connects them, and hands you straight back to your normal session.
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| Claude Code | Beta |
|
|
18
|
-
| OpenCode | Beta |
|
|
23
|
+
JuTell sits **beside** a coding agent you already use — Codex, Claude Code, or OpenCode. It checks
|
|
24
|
+
project facts before work and separates verified from assumed after it. It is not an AI model, and
|
|
25
|
+
it does not provide or replace your agent.
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
## The problem it solves
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
AI coding agents made writing code faster. They didn't fix two older problems:
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
- **Before work** — it's hard to explain exactly what you mean, and an agent that guesses wrong can
|
|
32
|
+
quietly do the wrong thing.
|
|
33
|
+
- **After work** — a wall of diffs and "I tested it" don't tell you what's real and what's assumed.
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
<summary>Build from source instead (contributors / verifying the repo directly)</summary>
|
|
35
|
+
JuTell doesn't write code. It makes the conversation around the code honest, on both ends.
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
## What it actually looks like
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
A real first run, on a project with OpenCode installed:
|
|
40
|
+
|
|
41
|
+
```console
|
|
42
|
+
$ jutell
|
|
43
|
+
JuTell
|
|
44
|
+
|
|
45
|
+
Found coding agents:
|
|
46
|
+
|
|
47
|
+
Codex not detected
|
|
48
|
+
OpenCode found
|
|
49
|
+
Claude Code not detected
|
|
50
|
+
|
|
51
|
+
Connecting JuTell...
|
|
52
|
+
OpenCode connected.
|
|
53
|
+
|
|
54
|
+
Open a new conversation in OpenCode and JuTell applies automatically.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then check it honestly — note that it does **not** claim the agent session picked it up, because it
|
|
58
|
+
cannot see that from here:
|
|
59
|
+
|
|
60
|
+
```console
|
|
61
|
+
$ jutell status
|
|
62
|
+
JuTell status
|
|
63
|
+
|
|
64
|
+
CLI: 2.0.1
|
|
65
|
+
Skill: installed
|
|
66
|
+
AGENTS.md: JuTell block present
|
|
67
|
+
OpenCode MCP: enabled (auto-start on new session)
|
|
68
|
+
Codex MCP: not registered
|
|
69
|
+
Current agent session applied: needs direct confirmation
|
|
70
|
+
Profile: balanced
|
|
71
|
+
Telemetry: disabled
|
|
72
|
+
External transmission: none
|
|
36
73
|
```
|
|
37
|
-
</details>
|
|
38
74
|
|
|
39
|
-
|
|
75
|
+
And when something looks wrong:
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
$ jutell doctor
|
|
79
|
+
OK Node version: Node 22.22.1
|
|
80
|
+
OK Skill file: verified
|
|
81
|
+
OK Skill version: 2.0.1 (installed copy matches)
|
|
82
|
+
OK MCP server live connection (Stdio): JuTell server responded with 5 tools
|
|
83
|
+
OK External transmission code: no outbound patterns in the MCP build
|
|
84
|
+
Check Current agent session applied: must be confirmed in that agent's session
|
|
85
|
+
Warning Codex MCP: not registered
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`doctor` marks every line OK / warning / needs-a-closer-look, and never prints full paths or secret
|
|
89
|
+
values.
|
|
90
|
+
|
|
91
|
+
## The rule it follows
|
|
92
|
+
|
|
93
|
+
JuTell does not call something "verified" unless it actually checked it. On a real run that changed
|
|
94
|
+
an empty-search message's styling, it reported:
|
|
95
|
+
|
|
96
|
+
| | |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Code checked | the style values actually changed |
|
|
99
|
+
| Test checked | the existing test still passed |
|
|
100
|
+
| Real browser view | **not checked** — no browser was available |
|
|
101
|
+
| Your action | Open the screen once and look. |
|
|
102
|
+
|
|
103
|
+
It does not turn "looks right in code" into "confirmed on screen."
|
|
104
|
+
|
|
105
|
+
If you are verifying the source instead of installing from npm, `npm pack` in
|
|
106
|
+
`packages/cli` creates `jutell-2.0.1.tgz`; ordinary users do not need this path.
|
|
107
|
+
|
|
108
|
+
## Supported agents & platforms
|
|
109
|
+
|
|
110
|
+
| What you need | Status |
|
|
111
|
+
|---|---|
|
|
112
|
+
| **Codex** | Supported |
|
|
113
|
+
| **Claude Code** | Beta |
|
|
114
|
+
| **OpenCode** | Beta |
|
|
115
|
+
| Windows | Tested |
|
|
116
|
+
| Ubuntu | Limited testing |
|
|
117
|
+
| macOS | Available / unverified |
|
|
118
|
+
|
|
119
|
+
You need a coding agent first — JuTell connects to one you already installed.
|
|
120
|
+
|
|
121
|
+
## Commands
|
|
122
|
+
|
|
123
|
+
| Command | What it does |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `jutell` | Finds installed agents and offers to connect them. |
|
|
126
|
+
| `jutell status` | Installation, connection, profile, and feature status. |
|
|
127
|
+
| `jutell doctor` | Checks for setup problems. |
|
|
128
|
+
| `jutell use codex` / `claude` / `opencode` | Connect one agent by hand (repair path). |
|
|
129
|
+
| `jutell on` / `jutell off` | Turn the connection on or off. |
|
|
130
|
+
| `jutell upgrade` | Refresh the installed Skill/config/MCP. |
|
|
131
|
+
| `jutell uninstall` | Remove JuTell's managed setup. |
|
|
132
|
+
|
|
133
|
+
Reports can be tuned with a project `.jutell.json` — profiles `minimal`, `balanced`, `learning`,
|
|
134
|
+
`detailed` change explanation length, never the underlying facts or risk.
|
|
135
|
+
|
|
136
|
+
## First things to try
|
|
137
|
+
|
|
138
|
+
1. "Change the login button to blue."
|
|
139
|
+
2. "Please make signup simpler."
|
|
140
|
+
3. "Make the empty search box show a helpful message."
|
|
141
|
+
|
|
142
|
+
For 1 and 3, JuTell should let your agent get on with it. For "make signup simpler," it should read
|
|
143
|
+
the project first and ask **one** grounded question only if a real choice remains — not turn your
|
|
144
|
+
request into a questionnaire.
|
|
145
|
+
|
|
146
|
+
## What JuTell is — and is not
|
|
147
|
+
|
|
148
|
+
**Is:** a communication layer beside your coding agent · clarifies material intent before work ·
|
|
149
|
+
explains actual work and evidence after it.
|
|
150
|
+
|
|
151
|
+
**Is not:** an AI model · a replacement coding agent · a correctness guarantee · a full autonomous
|
|
152
|
+
debugger · a multi-agent orchestrator.
|
|
153
|
+
|
|
154
|
+
## Trust and privacy
|
|
155
|
+
|
|
156
|
+
JuTell explains work using files, Git, and command output already on your computer. It does not
|
|
157
|
+
collect or send your project code, prompts, raw agent answers, diffs, or secrets. Telemetry is off
|
|
158
|
+
by default and its storage/transmission is not implemented at this stage.
|
|
159
|
+
|
|
160
|
+
## Install trouble on Ubuntu/Linux
|
|
161
|
+
|
|
162
|
+
If `npm install -g jutell` fails with `EACCES` / permission denied, that's a common npm global
|
|
163
|
+
folder ownership issue, not something specific to JuTell. **Avoid `sudo npm install -g jutell`** —
|
|
164
|
+
it leaves root-owned files that cause the same problem again. Two safe fixes:
|
|
165
|
+
[Ubuntu/Linux install permission error](https://github.com/ju0o/jutell/blob/main/docs/CLI_INSTALLATION.md).
|
|
166
|
+
|
|
167
|
+
## 한국어
|
|
168
|
+
|
|
169
|
+
전체 한국어 문서는 [README.ko.md](https://github.com/ju0o/jutell/blob/main/README.ko.md)에 있습니다.
|
|
170
|
+
비개발자도 "승인해도 되는가", "무엇을 직접 확인해야 하는가"를 판단할 수 있게 하는 것이 목적입니다.
|
|
171
|
+
|
|
172
|
+
## More
|
|
173
|
+
|
|
174
|
+
Full walkthrough with images, a real report, a real failure, and an honest efficiency snapshot:
|
|
175
|
+
[GitHub README](https://github.com/ju0o/jutell#readme) ·
|
|
176
|
+
[2.0.0 release showcase](https://github.com/ju0o/jutell/blob/main/docs/releases/2.0.0-showcase.md) ·
|
|
177
|
+
[Changelog](https://github.com/ju0o/jutell/blob/main/CHANGELOG.md) ·
|
|
178
|
+
[MCP integration](https://github.com/ju0o/jutell/blob/main/docs/MCP_INTEGRATION.md)
|
|
179
|
+
|
|
180
|
+
## Support
|
|
181
|
+
|
|
182
|
+
JuTell is free and MIT-licensed, and it stays that way. If it saved you time:
|
|
183
|
+
|
|
184
|
+
[](https://ko-fi.com/ju0o___)
|
|
185
|
+
|
|
186
|
+
Not supporting changes nothing — every feature stays available to everyone.
|
|
@@ -5,7 +5,7 @@ import { activeFeatures, beginnerReportRules, bridgeStatus, reportPreferences, s
|
|
|
5
5
|
import { recordToolCall } from './tools/usage-counters.js';
|
|
6
6
|
const server = new McpServer({
|
|
7
7
|
name: 'JuTell',
|
|
8
|
-
version: '2.0.
|
|
8
|
+
version: '2.0.1',
|
|
9
9
|
}, {
|
|
10
10
|
instructions: 'JuTell by Ju0 is a local read-only report helper. Read only project configuration and approved report rules. Never access project code, Git diff, prompts, AI answers, secrets, or external networks. Skill mode remains available if this MCP server is disabled or unavailable. When both jutell and beginner_bridge servers are visible, prefer the canonical jutell server; use beginner_bridge only for compatibility. For owner-facing reports, apply the JuTell reporting guidance before composing the final answer. Prefer these tools over re-reading the JuTell Skill reference files when both are available, since a tool call returns the same project-specific rules in one step. Call get_beginner_report_rules once, at task completion, right before writing the final report — not after every file read, shell command, or edit, and not to verify work that is already done. If these tools are unavailable or blocked, fall back to the JuTell Skill files without interrupting the task, and never tell the user JuTell MCP was used unless a JuTell tool call actually returned a result in this task.',
|
|
11
11
|
});
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: beginner-bridge
|
|
3
|
-
jutellSkillVersion: "2.0.
|
|
3
|
+
jutellSkillVersion: "2.0.1"
|
|
4
4
|
schemaVersion: 1
|
|
5
5
|
description: JuTell by Ju0 creates concise, evidence-based work reports for non-developers, separating observed facts, code-based expectations, verification results, risks, and user actions. The legacy Skill ID is retained for compatibility.
|
|
6
6
|
---
|
package/assets/version.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { useCommand, connectCommand, disconnectCommand, switchCommand } from './
|
|
|
11
11
|
import { sessionCommand } from './commands/session/index.js';
|
|
12
12
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
13
13
|
import { migrateCommand } from './commands/migrate.js';
|
|
14
|
+
import { maybeShowFundingNotice } from './output/funding.js';
|
|
14
15
|
function safeError(message, verbose) {
|
|
15
16
|
if (verbose)
|
|
16
17
|
return message;
|
|
@@ -21,6 +22,9 @@ function safeError(message, verbose) {
|
|
|
21
22
|
export async function run(argv = process.argv.slice(2), io = createIo(), legacyAlias = false) {
|
|
22
23
|
if (legacyAlias)
|
|
23
24
|
io.write('`beginner-bridge`는 이전 명령입니다. 앞으로는 `jutell` 사용을 권장합니다.');
|
|
25
|
+
const fundingSuppressed = argv.includes('--no-funding');
|
|
26
|
+
if (fundingSuppressed)
|
|
27
|
+
argv = argv.filter((a) => a !== '--no-funding');
|
|
24
28
|
if (argv.includes('--version')) {
|
|
25
29
|
io.write((await readVersionInfo()).cli);
|
|
26
30
|
return 0;
|
|
@@ -72,6 +76,7 @@ export async function run(argv = process.argv.slice(2), io = createIo(), legacyA
|
|
|
72
76
|
await sessionCommand(paths, options, io, extraArgs);
|
|
73
77
|
else
|
|
74
78
|
throw new Error(`알 수 없는 명령입니다: ${command}`);
|
|
79
|
+
maybeShowFundingNotice(paths, io, fundingSuppressed);
|
|
75
80
|
return 0;
|
|
76
81
|
}
|
|
77
82
|
catch (error) {
|
package/dist/commands/migrate.js
CHANGED
|
@@ -102,11 +102,27 @@ export async function migrateCommand(paths, options, io) {
|
|
|
102
102
|
const legacyPattern = /# BEGINNER_BRIDGE_CLI_MCP_BEGIN[\s\S]*?# BEGINNER_BRIDGE_CLI_MCP_END\n?/m;
|
|
103
103
|
const legacyPattern2 = /# BEGINNER_BRIDGE_MCP_BEGIN[\s\S]*?# BEGINNER_BRIDGE_MCP_END\n?/m;
|
|
104
104
|
let next = text.replace(legacyPattern, '').replace(legacyPattern2, '');
|
|
105
|
-
// Also remove unmarked legacy with heuristic: if beginner_bridge still present but not in managed block, check evidence
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
// Also remove unmarked legacy with heuristic: if beginner_bridge still present but not in managed block, check evidence.
|
|
106
|
+
// Use indexOf (not a `\s*`-prefixed regex .search()) to find the header: `^\s*\[...\]` lets
|
|
107
|
+
// `\s*` swallow a preceding blank line, so .search() can return an index *before* the literal
|
|
108
|
+
// `[` - e.g. when `use codex` leaves a blank-line separator above this block (its normal
|
|
109
|
+
// output shape). `after.slice(1)` below then only strips 1 of those whitespace chars, lands
|
|
110
|
+
// back inside the same header, and "the next header" it finds is this one again, so nothing
|
|
111
|
+
// ever gets cut. indexOf always anchors exactly at `[`, where slice(1) is meant to start from.
|
|
112
|
+
const beginnerHeader = '[mcp_servers.beginner_bridge]';
|
|
113
|
+
const headerIdx = next.indexOf(beginnerHeader);
|
|
114
|
+
if (headerIdx >= 0) {
|
|
115
|
+
// Bound the section to *this table only* (up to the next `[section]` header,
|
|
116
|
+
// the canonical marker, or EOF) before doing anything else with it. The
|
|
117
|
+
// evidence check below must only ever look inside that bound - a flat N-char
|
|
118
|
+
// lookahead from the header (the previous approach) reads past this table's
|
|
119
|
+
// own end into whatever comes next in the file, and a JuTell-managed config
|
|
120
|
+
// almost always has the real `assets/mcp-server` canonical entry sitting
|
|
121
|
+
// right after the legacy one - so that flat window would find canonical's
|
|
122
|
+
// path and treat it as evidence for the *unrelated* entry above it, deleting
|
|
123
|
+
// a genuinely unrelated user-owned `beginner_bridge` server that merely
|
|
124
|
+
// happens to sit next to JuTell's own block in the same file.
|
|
125
|
+
const after = next.slice(headerIdx);
|
|
110
126
|
const nextHeader = after.slice(1).search(/^\s*\[mcp_servers\./m);
|
|
111
127
|
const nextMarker = after.search(/#\s*JUTELL_CLI_MCP_BEGIN/m);
|
|
112
128
|
let cut = after.length;
|
|
@@ -114,7 +130,10 @@ export async function migrateCommand(paths, options, io) {
|
|
|
114
130
|
cut = Math.min(cut, nextHeader + 1);
|
|
115
131
|
if (nextMarker >= 0)
|
|
116
132
|
cut = Math.min(cut, nextMarker);
|
|
117
|
-
|
|
133
|
+
const ownSection = after.slice(0, cut);
|
|
134
|
+
if (/(?:assets|apps)[\\/]mcp-server/i.test(ownSection)) {
|
|
135
|
+
next = next.slice(0, headerIdx) + after.slice(cut);
|
|
136
|
+
}
|
|
118
137
|
}
|
|
119
138
|
next = next.replace(/\n{3,}/g, '\n\n').trim();
|
|
120
139
|
await writeTextSafely(file, next ? `${next}\n` : '');
|
package/dist/commands/status.js
CHANGED
|
@@ -55,14 +55,16 @@ export async function getStatus(paths) {
|
|
|
55
55
|
const warnings = [];
|
|
56
56
|
if (!config.valid)
|
|
57
57
|
warnings.push('설정 파일을 읽지 못해 balanced 기본값을 사용 중입니다.');
|
|
58
|
+
if (config.invalidLimitsFields.length)
|
|
59
|
+
warnings.push(`.jutell.json의 limits 값 중 숫자가 아닌 항목이 있어 기본값을 대신 사용했습니다: ${config.invalidLimitsFields.join(', ')}. 실제 파일 값은 바뀌지 않았으니 직접 고쳐주세요.`);
|
|
58
60
|
if (registration.conflict)
|
|
59
61
|
warnings.push('같은 이름의 관리되지 않는 Codex MCP 설정이 있어 자동 변경하지 않았습니다.');
|
|
60
62
|
if (opencode.conflict)
|
|
61
63
|
warnings.push('OpenCode 설정에 같은 이름의 관리되지 않는 MCP 항목이 있어 자동 변경하지 않았습니다.');
|
|
62
64
|
if (registration.bothRegistered)
|
|
63
|
-
warnings.push('Codex에 canonical jutell과 legacy beginner_bridge MCP가 모두 있습니다. 자동 정리하지 않았습니다. 이전
|
|
65
|
+
warnings.push('Codex에 canonical jutell과 legacy beginner_bridge MCP가 모두 있습니다. 자동 정리하지 않았습니다. 이전 항목을 정리하려면 jutell migrate --clean 을 실행하세요.');
|
|
64
66
|
if (opencode.bothRegistered)
|
|
65
|
-
warnings.push('OpenCode에 canonical jutell과 legacy beginner_bridge MCP가 모두 있습니다. 자동 정리하지 않았습니다. 이전
|
|
67
|
+
warnings.push('OpenCode에 canonical jutell과 legacy beginner_bridge MCP가 모두 있습니다. 자동 정리하지 않았습니다. 이전 항목을 정리하려면 jutell migrate --clean 을 실행하세요.');
|
|
66
68
|
if (registration.legacyRegistered && !registration.canonicalRegistered)
|
|
67
69
|
warnings.push('Codex에 이전 beginner_bridge 항목만 있습니다. jutell use codex 를 실행하면 보존하면서 새 jutell 항목을 추가합니다.');
|
|
68
70
|
if (opencode.legacyRegistered && !opencode.canonicalRegistered)
|
|
@@ -176,9 +178,14 @@ export async function getDoctorResults(paths) {
|
|
|
176
178
|
checks.push({ name: 'Claude Code MCP', status: claude.registered ? '정상' : '주의', detail: claude.registered ? `${claude.claudeScope} 범위(${claude.claudeScope === 'user' ? '사용자 전역' : '현재 프로젝트'})에 등록되어 있습니다.` : 'Claude Code MCP가 등록되지 않았습니다.' });
|
|
177
179
|
checks.push({ name: config.source === 'legacy' ? '.beginner-bridge.json' : '.jutell.json', status: config.valid ? '정상' : '오류', detail: config.exists ? (config.valid ? (config.source === 'legacy' ? '이전 설정 파일을 읽었습니다. 새 .jutell.json이 없으면 사용합니다.' : '설정 형식을 확인했습니다.') : '설정이 올바르지 않아 기본값을 사용합니다.') : '없으면 기본 설정을 사용합니다.' });
|
|
178
180
|
const featuresValid = Object.keys(config.config.features).every((id) => FEATURE_IDS.includes(id));
|
|
179
|
-
|
|
181
|
+
// Range check runs on the already-normalized (defaulted) values, so it can never itself
|
|
182
|
+
// fail - normalizeConfig() only ever produces values inside these ranges. Fold in
|
|
183
|
+
// invalidLimitsFields (computed from the raw, pre-normalization file) so a malformed
|
|
184
|
+
// value that got silently replaced with its default is still reported as unhealthy,
|
|
185
|
+
// not "정상" for a file whose actual on-disk content doesn't match what's checked.
|
|
186
|
+
const limitsValid = config.config.limits.maxMainFiles >= 1 && config.config.limits.maxMainFiles <= 10 && config.config.limits.maxGlossaryTerms >= 0 && config.config.limits.maxGlossaryTerms <= 10 && config.config.limits.compactReportMaxSentences >= 4 && config.config.limits.compactReportMaxSentences <= 30 && config.invalidLimitsFields.length === 0;
|
|
180
187
|
checks.push({ name: '공식 Feature ID', status: featuresValid ? '정상' : '오류', detail: featuresValid ? '현재 공식 ID만 확인했습니다.' : '지원하지 않는 Feature ID가 있습니다.' });
|
|
181
|
-
checks.push({ name: 'limits', status: limitsValid ? '정상' : '오류', detail: limitsValid ? '허용 범위를 확인했습니다.' : '허용 범위를 벗어난 값이 있습니다.' });
|
|
188
|
+
checks.push({ name: 'limits', status: limitsValid ? '정상' : '오류', detail: config.invalidLimitsFields.length ? `숫자가 아닌 값이 있어 기본값을 대신 사용했습니다: ${config.invalidLimitsFields.join(', ')}.` : limitsValid ? '허용 범위를 확인했습니다.' : '허용 범위를 벗어난 값이 있습니다.' });
|
|
182
189
|
checks.push({ name: '로컬 관리자 빌드', status: await exists(adminEntry) ? '정상' : '오류', detail: await exists(adminEntry) ? '관리자 화면 파일을 확인했습니다.' : '관리자 화면 파일이 없습니다.' });
|
|
183
190
|
checks.push({ name: '포트 사용 가능 여부', status: await portAvailable() ? '정상' : '주의', detail: '127.0.0.1의 임시 포트를 확인했습니다.' });
|
|
184
191
|
checks.push({ name: '쓰기 권한', status: await writeCheck(paths) ? '정상' : '오류', detail: '로컬 상태 폴더에 임시 파일을 만들고 삭제했습니다.' });
|
package/dist/commands/use.js
CHANGED
|
@@ -2,7 +2,7 @@ import { assets, codexScopedPaths, packageRoot } from '../config/paths.js';
|
|
|
2
2
|
import { readCodexRegistration, registerMcp, snapshot, restore } from '../config/managed.js';
|
|
3
3
|
import { ensureBridgeConfig, setMcpEnabled } from '../installer/config.js';
|
|
4
4
|
import { installSkill, recordSkillFiles, removeAddedSkillFiles } from '../installer/skill.js';
|
|
5
|
-
import { agentsFile, ensureJuTellAgentsBlock } from '../installer/agents.js';
|
|
5
|
+
import { agentsFile, claudeMdFile, ensureJuTellAgentsBlock } from '../installer/agents.js';
|
|
6
6
|
import { opencodeDetected, readOpenCodeRegistration, registerOpenCodeMcp, setOpenCodeEnabled } from '../installer/opencode.js';
|
|
7
7
|
import { readClaudeRegistration, registerClaudeMcp, removeClaudeMcp } from '../installer/claude.js';
|
|
8
8
|
import { findProvider, supportedProviderNames } from '../installer/providers.js';
|
|
@@ -54,8 +54,12 @@ async function resolveTarget(args, io) {
|
|
|
54
54
|
async function registrationSnapshots(paths) {
|
|
55
55
|
const opencode = await readOpenCodeRegistration(paths, packageRoot(), false);
|
|
56
56
|
const files = [paths.configFile, paths.codexConfigFile, codexScopedPaths(paths).codexConfigFile, opencode.file, paths.claudeConfigFile];
|
|
57
|
+
// registerClaudeMcp writes CLAUDE.md before touching the MCP entry (see its
|
|
58
|
+
// comment) - snapshot it too so a failure partway through `use claude`
|
|
59
|
+
// rolls it back along with AGENTS.md instead of leaving it added while the
|
|
60
|
+
// MCP registration itself got rolled back.
|
|
57
61
|
if (paths.scope === 'project')
|
|
58
|
-
files.push(agentsFile(paths.targetRoot));
|
|
62
|
+
files.push(agentsFile(paths.targetRoot), claudeMdFile(paths.targetRoot));
|
|
59
63
|
return Promise.all(files.map((file) => snapshot(file)));
|
|
60
64
|
}
|
|
61
65
|
async function registerProviderEnabled(paths, provider, io) {
|
|
@@ -63,7 +67,7 @@ async function registerProviderEnabled(paths, provider, io) {
|
|
|
63
67
|
await adapter.register(paths, true);
|
|
64
68
|
const current = await adapter.read(paths, true);
|
|
65
69
|
if (current.canonicalRegistered && current.legacyRegistered) {
|
|
66
|
-
io.write('\n이전 beginner_bridge 항목을 그대로 두고 새 jutell 항목을 추가했습니다.\n이전 항목은 자동으로 삭제하지 않습니다.
|
|
70
|
+
io.write('\n이전 beginner_bridge 항목을 그대로 두고 새 jutell 항목을 추가했습니다.\n이전 항목은 자동으로 삭제하지 않습니다. 정리하려면 jutell migrate --clean 을 실행하세요.');
|
|
67
71
|
}
|
|
68
72
|
if (provider.id === 'codex') {
|
|
69
73
|
io.write('\nCodex는 MCP 서버 목록을 사용자 전역 설정에서만 읽습니다.\nJuTell 프로젝트 규칙(AGENTS.md, Skill, 설정)은 이 프로젝트에 그대로 두고,\nCodex MCP 연결만 사용자 전역 설정(Codex 홈)에 등록했습니다.');
|
package/dist/config/managed.js
CHANGED
|
@@ -109,19 +109,40 @@ export function normalizeConfig(value) {
|
|
|
109
109
|
voice: { preset: voicePreset },
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
const LIMITS_KEYS = ['maxMainFiles', 'maxGlossaryTerms', 'compactReportMaxSentences'];
|
|
113
|
+
// normalizeConfig() silently substitutes the schema default for any limits field that
|
|
114
|
+
// isn't a valid integer - correct for making the CLI keep working, but it never signals
|
|
115
|
+
// that the substitution happened, so a user's own (invalid) edit is silently overridden
|
|
116
|
+
// with no way to notice. Computed separately here (readBridgeConfig has the raw parsed
|
|
117
|
+
// object; normalizeConfig only returns the already-normalized result) so status/doctor
|
|
118
|
+
// can warn about it without changing normalizeConfig's own return shape or call sites.
|
|
119
|
+
function invalidLimitsFields(parsed) {
|
|
120
|
+
if (!('limits' in parsed))
|
|
121
|
+
return []; // never set - nothing of the user's is being overridden
|
|
122
|
+
const limits = parsed.limits;
|
|
123
|
+
// `limits` present but not a plain object (a string, array, number, boolean, or null)
|
|
124
|
+
// is the same silent-override problem one level up: normalizeConfig()'s own
|
|
125
|
+
// `input.limits && typeof === 'object' && !Array.isArray` guard treats any of these
|
|
126
|
+
// as if `limits` were `{}` and defaults every field - so report all three as invalid
|
|
127
|
+
// rather than picking apart a shape that was never a fields-object to begin with.
|
|
128
|
+
if (!limits || typeof limits !== 'object' || Array.isArray(limits))
|
|
129
|
+
return [...LIMITS_KEYS];
|
|
130
|
+
const record = limits;
|
|
131
|
+
return LIMITS_KEYS.filter((key) => key in record && !(typeof record[key] === 'number' && Number.isInteger(record[key])));
|
|
132
|
+
}
|
|
112
133
|
export async function readBridgeConfig(paths) {
|
|
113
134
|
const preferred = await readText(paths.configFile);
|
|
114
135
|
const raw = preferred ?? await readText(paths.legacyConfigFile);
|
|
115
136
|
const source = preferred !== undefined ? 'new' : raw !== undefined ? 'legacy' : 'default';
|
|
116
137
|
if (!raw)
|
|
117
|
-
return { config: await defaultConfig(), exists: false, valid: true, source };
|
|
138
|
+
return { config: await defaultConfig(), exists: false, valid: true, source, invalidLimitsFields: [] };
|
|
118
139
|
try {
|
|
119
140
|
const parsed = JSON.parse(raw);
|
|
120
141
|
const valid = parsed.version === 1 && typeof parsed.profile === 'string' && PROFILES.includes(parsed.profile);
|
|
121
|
-
return { config: normalizeConfig(parsed), exists: true, valid, source };
|
|
142
|
+
return { config: normalizeConfig(parsed), exists: true, valid, source, invalidLimitsFields: invalidLimitsFields(parsed) };
|
|
122
143
|
}
|
|
123
144
|
catch {
|
|
124
|
-
return { config: await defaultConfig(), exists: true, valid: false, source };
|
|
145
|
+
return { config: await defaultConfig(), exists: true, valid: false, source, invalidLimitsFields: [] };
|
|
125
146
|
}
|
|
126
147
|
}
|
|
127
148
|
export async function writeBridgeConfig(paths, config) {
|
package/dist/installer/agents.js
CHANGED
|
@@ -19,12 +19,22 @@ function markerPattern() {
|
|
|
19
19
|
export function agentsFile(projectRoot) {
|
|
20
20
|
return path.join(projectRoot, 'AGENTS.md');
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Claude Code does not auto-discover `AGENTS.md` the way Codex/OpenCode do - it
|
|
23
|
+
// auto-loads `CLAUDE.md` instead (verified empirically: a project with only an
|
|
24
|
+
// AGENTS.md canary instruction was never followed, the same canary in CLAUDE.md
|
|
25
|
+
// always was). Without this, `jutell use claude` reports a healthy MCP
|
|
26
|
+
// connection while the agent never learns to read SKILL.md or call jutell_*
|
|
27
|
+
// tools, because the one file telling it to do that sits somewhere Claude Code
|
|
28
|
+
// doesn't automatically read. Same managed block, same markers, second file -
|
|
29
|
+
// see `ensureJuTellClaudeMdBlock` below, wired in from installer/claude.ts.
|
|
30
|
+
export function claudeMdFile(projectRoot) {
|
|
31
|
+
return path.join(projectRoot, 'CLAUDE.md');
|
|
32
|
+
}
|
|
33
|
+
async function hasManagedBlock(file) {
|
|
34
|
+
const content = await readText(file);
|
|
24
35
|
return Boolean(content && markerPattern().test(content));
|
|
25
36
|
}
|
|
26
|
-
|
|
27
|
-
const file = agentsFile(projectRoot);
|
|
37
|
+
async function ensureManagedBlock(file) {
|
|
28
38
|
const current = await readText(file) ?? '';
|
|
29
39
|
const next = markerPattern().test(current)
|
|
30
40
|
? current.replace(markerPattern(), managedBlock).replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
@@ -33,8 +43,7 @@ export async function ensureJuTellAgentsBlock(projectRoot) {
|
|
|
33
43
|
await writeTextSafely(file, next);
|
|
34
44
|
return { changed: next !== current };
|
|
35
45
|
}
|
|
36
|
-
|
|
37
|
-
const file = agentsFile(projectRoot);
|
|
46
|
+
async function removeManagedBlock(file) {
|
|
38
47
|
const current = await readText(file);
|
|
39
48
|
if (!current || !markerPattern().test(current))
|
|
40
49
|
return { changed: false };
|
|
@@ -42,3 +51,21 @@ export async function removeJuTellAgentsBlock(projectRoot) {
|
|
|
42
51
|
await writeTextSafely(file, next ? `${next}\n` : '');
|
|
43
52
|
return { changed: true };
|
|
44
53
|
}
|
|
54
|
+
export async function hasJuTellAgentsBlock(projectRoot) {
|
|
55
|
+
return hasManagedBlock(agentsFile(projectRoot));
|
|
56
|
+
}
|
|
57
|
+
export async function ensureJuTellAgentsBlock(projectRoot) {
|
|
58
|
+
return ensureManagedBlock(agentsFile(projectRoot));
|
|
59
|
+
}
|
|
60
|
+
export async function removeJuTellAgentsBlock(projectRoot) {
|
|
61
|
+
return removeManagedBlock(agentsFile(projectRoot));
|
|
62
|
+
}
|
|
63
|
+
export async function hasJuTellClaudeMdBlock(projectRoot) {
|
|
64
|
+
return hasManagedBlock(claudeMdFile(projectRoot));
|
|
65
|
+
}
|
|
66
|
+
export async function ensureJuTellClaudeMdBlock(projectRoot) {
|
|
67
|
+
return ensureManagedBlock(claudeMdFile(projectRoot));
|
|
68
|
+
}
|
|
69
|
+
export async function removeJuTellClaudeMdBlock(projectRoot) {
|
|
70
|
+
return removeManagedBlock(claudeMdFile(projectRoot));
|
|
71
|
+
}
|
package/dist/installer/claude.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readText } from '../config/managed.js';
|
|
4
4
|
import { claudeHome } from '../config/paths.js';
|
|
5
|
+
import { ensureJuTellClaudeMdBlock, removeJuTellClaudeMdBlock } from './agents.js';
|
|
5
6
|
export const CLAUDE_MCP_KEY = 'jutell';
|
|
6
7
|
function normalizeForCompare(value) {
|
|
7
8
|
return value.replace(/\\/g, '/').toLowerCase();
|
|
@@ -119,6 +120,14 @@ function runClaude(args, paths) {
|
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
122
|
export async function registerClaudeMcp(paths, packageRoot, enabled) {
|
|
123
|
+
// Claude Code auto-loads `CLAUDE.md`, not `AGENTS.md` (verified empirically -
|
|
124
|
+
// see the comment on ensureJuTellClaudeMdBlock). Without a managed CLAUDE.md,
|
|
125
|
+
// the MCP server below connects fine but the agent never learns to read
|
|
126
|
+
// SKILL.md or call jutell_* tools, since AGENTS.md alone isn't guaranteed to
|
|
127
|
+
// be discovered. Ensured unconditionally (before the idempotent early-return
|
|
128
|
+
// below) so a repeat `use claude` still restores it if a user deleted it.
|
|
129
|
+
if (paths.scope === 'project')
|
|
130
|
+
await ensureJuTellClaudeMdBlock(paths.targetRoot);
|
|
122
131
|
const current = await readClaudeRegistration(paths, packageRoot, enabled);
|
|
123
132
|
const config = await readClaudeConfig(paths);
|
|
124
133
|
if (config === undefined)
|
|
@@ -147,6 +156,13 @@ export async function registerClaudeMcp(paths, packageRoot, enabled) {
|
|
|
147
156
|
}
|
|
148
157
|
export async function removeClaudeMcp(paths, packageRoot) {
|
|
149
158
|
const current = await readClaudeRegistration(paths, packageRoot, false);
|
|
159
|
+
// CLAUDE.md is Claude-specific (no other provider reads it, same way only
|
|
160
|
+
// OpenCode reads its own opencode.json mcp block) - unlike AGENTS.md, which
|
|
161
|
+
// stays shared across providers and is only ever touched by disable/uninstall.
|
|
162
|
+
// Removed here unconditionally so disconnect/switch/uninstall - every caller
|
|
163
|
+
// of removeClaudeMcp - clean it up too, not just the MCP entry.
|
|
164
|
+
if (paths.scope === 'project')
|
|
165
|
+
await removeJuTellClaudeMdBlock(paths.targetRoot);
|
|
150
166
|
if (!current.registered)
|
|
151
167
|
return current;
|
|
152
168
|
try {
|
package/dist/output/format.js
CHANGED
|
@@ -140,7 +140,7 @@ export function parseOptions(args) {
|
|
|
140
140
|
}
|
|
141
141
|
export function scopeLabel(scope) { return scope === 'global' ? '사용자 전역' : '현재 프로젝트'; }
|
|
142
142
|
export function printHelp(io) {
|
|
143
|
-
io.write(`JuTell CLI 2.0.
|
|
143
|
+
io.write(`JuTell CLI 2.0.1
|
|
144
144
|
|
|
145
145
|
시작할 때는 jutell만 입력하면 됩니다.
|
|
146
146
|
설치된 Coding Agent(Codex, OpenCode, Claude Code)를 찾아 연결하고,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const FUNDING_URL = 'https://ko-fi.com/ju0o___';
|
|
4
|
+
const MARKER_FILE = 'funding-notice-shown';
|
|
5
|
+
function truthy(value) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return false;
|
|
8
|
+
const normalized = value.trim().toLowerCase();
|
|
9
|
+
return normalized !== '' && normalized !== '0' && normalized !== 'false';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Shows the support line at most once per machine, and only in a real
|
|
13
|
+
* terminal. Never runs in CI, never blocks, and never changes the exit code:
|
|
14
|
+
* any failure here is swallowed so a funding message can't break a command.
|
|
15
|
+
*/
|
|
16
|
+
export function maybeShowFundingNotice(paths, io, suppressed) {
|
|
17
|
+
try {
|
|
18
|
+
if (suppressed)
|
|
19
|
+
return;
|
|
20
|
+
if (truthy(process.env.JUTELL_NO_FUNDING))
|
|
21
|
+
return;
|
|
22
|
+
if (truthy(process.env.CI))
|
|
23
|
+
return;
|
|
24
|
+
if (!process.stdout.isTTY)
|
|
25
|
+
return;
|
|
26
|
+
const marker = path.join(paths.dataRoot, MARKER_FILE);
|
|
27
|
+
if (fs.existsSync(marker))
|
|
28
|
+
return;
|
|
29
|
+
fs.mkdirSync(paths.dataRoot, { recursive: true });
|
|
30
|
+
fs.writeFileSync(marker, new Date().toISOString());
|
|
31
|
+
io.write('');
|
|
32
|
+
io.write(`JuTell은 무료이고 앞으로도 무료입니다. 도움이 되셨다면: ${FUNDING_URL}`);
|
|
33
|
+
io.write('이 안내는 다시 표시되지 않습니다. (끄기: JUTELL_NO_FUNDING=1 또는 --no-funding)');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// A support message must never affect the command result.
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/process/mcpProbe.js
CHANGED
|
@@ -6,7 +6,7 @@ const INITIALIZE = JSON.stringify({
|
|
|
6
6
|
jsonrpc: '2.0',
|
|
7
7
|
id: 1,
|
|
8
8
|
method: 'initialize',
|
|
9
|
-
params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'jutell-doctor', version: '2.0.
|
|
9
|
+
params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'jutell-doctor', version: '2.0.1' } },
|
|
10
10
|
});
|
|
11
11
|
const INITIALIZED = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
12
12
|
const TOOLS_LIST = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jutell",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Understand what your AI coding agent actually did - a clarify-before / verify-after layer for Codex, Claude Code and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -11,16 +11,31 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ju0o/jutell/issues"
|
|
13
13
|
},
|
|
14
|
+
"funding": {
|
|
15
|
+
"type": "ko_fi",
|
|
16
|
+
"url": "https://ko-fi.com/ju0o___"
|
|
17
|
+
},
|
|
14
18
|
"keywords": [
|
|
15
19
|
"jutell",
|
|
16
20
|
"ai",
|
|
21
|
+
"ai-agent",
|
|
22
|
+
"coding-agent",
|
|
17
23
|
"agent",
|
|
18
24
|
"harness",
|
|
19
|
-
"mcp",
|
|
20
|
-
"skill",
|
|
21
25
|
"codex",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"claude",
|
|
22
28
|
"opencode",
|
|
23
|
-
"
|
|
29
|
+
"mcp",
|
|
30
|
+
"mcp-server",
|
|
31
|
+
"skill",
|
|
32
|
+
"cli",
|
|
33
|
+
"developer-tools",
|
|
34
|
+
"ai-tools",
|
|
35
|
+
"llm",
|
|
36
|
+
"code-review",
|
|
37
|
+
"vibe-coding",
|
|
38
|
+
"korean"
|
|
24
39
|
],
|
|
25
40
|
"author": "Ju0",
|
|
26
41
|
"engines": {
|