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 CHANGED
@@ -1,39 +1,186 @@
1
- # jutell
1
+ # JuTell
2
2
 
3
- **Tell your coding agent what you actually mean — and understand what it actually did.**
3
+ **Tell your agent what you mean. Understand what it did.**
4
4
 
5
- Before work, JuTell clarifies only the decisions that would actually change the result (at most one concise question) and checks project facts itself. After work, it turns the result into a plain-language report: what changed, what's actually verified, what's still unknown, and what to do next. It never guarantees your agent's answers — it keeps both directions honest.
5
+ [![npm version](https://img.shields.io/npm/v/jutell.svg)](https://www.npmjs.com/package/jutell)
6
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ju0o/jutell/blob/main/LICENSE)
7
+ [![node: >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
8
+ ![Codex: Supported](https://img.shields.io/badge/Codex-Supported-brightgreen)
9
+ ![Claude Code: Beta](https://img.shields.io/badge/Claude%20Code-Beta-yellow)
10
+ ![OpenCode: Beta](https://img.shields.io/badge/OpenCode-Beta-yellow)
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 you already have installed, asks for one approval, connects them, and hands you straight back to your normal session — no per-agent setup command needed for a normal first run.
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
- | Agent | Status |
15
- |---|---|
16
- | Codex | Supported |
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
- To connect (or reconnect) one specific agent by hand, use `jutell use codex` / `jutell use claude` / `jutell use opencode` — this is the manual/repair path, not the normal first run.
27
+ ## The problem it solves
21
28
 
22
- **한국어 사용자라면:** 전체 문서와 한국어 안내는 [GitHub 저장소의 README.ko.md](https://github.com/ju0o/jutell/blob/main/README.ko.md)에 있습니다.
29
+ AI coding agents made writing code faster. They didn't fix two older problems:
23
30
 
24
- Full docs, images, and the complete feature walkthrough live on [GitHub](https://github.com/ju0o/jutell#readme).
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
- <details>
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
- Most people should use the npm install above.
37
+ ## What it actually looks like
30
38
 
31
- ```bash
32
- cd packages/cli
33
- npm install
34
- npm pack
35
- npm install -g ./jutell-2.0.0.tgz
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
- The legacy `beginner-bridge` command is a compatibility alias for the same functionality and tells you to switch to `jutell` when you run it. The CLI does not collect or transmit your project code, prompts, AI answers, Git diffs, or secrets.
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
+ [![Support on Ko-fi](https://img.shields.io/badge/Ko--fi-support-FF5E5B?logo=ko-fi&logoColor=white)](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.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
  });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: beginner-bridge
3
- jutellSkillVersion: "2.0.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
  ---
@@ -1,5 +1,5 @@
1
1
  {
2
- "cli": "2.0.0",
2
+ "cli": "2.0.1",
3
3
  "skill": "확인 필요",
4
4
  "mcp": "0.1.0",
5
5
  "admin": "0.1.0"
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) {
@@ -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
- if (/^\s*\[mcp_servers\.beginner_bridge\]/m.test(next) && /(?:assets|apps)[\\/]mcp-server/i.test(next.slice(next.search(/^\s*\[mcp_servers\.beginner_bridge\]/m), next.search(/^\s*\[mcp_servers\.beginner_bridge\]/m) + 1200))) {
107
- // Remove that section (from header until next header/marker or end)
108
- const idx = next.search(/^\s*\[mcp_servers\.beginner_bridge\]/m);
109
- const after = next.slice(idx);
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
- next = next.slice(0, idx) + after.slice(cut);
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` : '');
@@ -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
- 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;
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: '로컬 상태 폴더에 임시 파일을 만들고 삭제했습니다.' });
@@ -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 홈)에 등록했습니다.');
@@ -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) {
@@ -19,12 +19,22 @@ function markerPattern() {
19
19
  export function agentsFile(projectRoot) {
20
20
  return path.join(projectRoot, 'AGENTS.md');
21
21
  }
22
- export async function hasJuTellAgentsBlock(projectRoot) {
23
- const content = await readText(agentsFile(projectRoot));
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
- export async function ensureJuTellAgentsBlock(projectRoot) {
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
- export async function removeJuTellAgentsBlock(projectRoot) {
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
+ }
@@ -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 {
@@ -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.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
+ }
@@ -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.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.0",
4
- "description": "JuTell by Ju0 — non-developer harness for AI coding agents (Skill, MCP, local dashboard)",
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
- "claude-code"
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": {