@moabpro/recap-mcp 0.1.0 → 0.1.2
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/dist/auth.js +6 -6
- package/dist/errors.js +1 -1
- package/dist/init.js +105 -15
- package/dist/server.js +1 -3
- package/dist/session-start.js +6 -2
- package/dist/tools/auth-tools.js +8 -8
- package/dist/tools/send-recap.js +1 -1
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -55,19 +55,19 @@ export class Authenticator {
|
|
|
55
55
|
async getAccessToken() {
|
|
56
56
|
const state = await this.store.load();
|
|
57
57
|
if (!state)
|
|
58
|
-
throw new AuthRequiredError('Не авторизовано. Запустите инструмент
|
|
58
|
+
throw new AuthRequiredError('Не авторизовано. Запустите инструмент moab_login.');
|
|
59
59
|
if (state.tokens && state.tokens.expiresAt - this.now() > EXPIRY_SKEW_MS)
|
|
60
60
|
return state.tokens.accessToken;
|
|
61
61
|
if (state.tokens)
|
|
62
62
|
return this.refreshOrThrow(state.tokens.refreshToken);
|
|
63
63
|
if (state.pending)
|
|
64
64
|
return this.completePendingOrThrow(state.pending);
|
|
65
|
-
throw new AuthRequiredError('Не авторизовано. Запустите инструмент
|
|
65
|
+
throw new AuthRequiredError('Не авторизовано. Запустите инструмент moab_login.');
|
|
66
66
|
}
|
|
67
67
|
async invalidateAndRefresh() {
|
|
68
68
|
const state = await this.store.load();
|
|
69
69
|
if (!state?.tokens)
|
|
70
|
-
throw new AuthRequiredError('Не авторизовано. Запустите инструмент
|
|
70
|
+
throw new AuthRequiredError('Не авторизовано. Запустите инструмент moab_login.');
|
|
71
71
|
return this.refreshOrThrow(state.tokens.refreshToken);
|
|
72
72
|
}
|
|
73
73
|
async status() {
|
|
@@ -85,7 +85,7 @@ export class Authenticator {
|
|
|
85
85
|
});
|
|
86
86
|
if (!res.ok) {
|
|
87
87
|
await this.store.clear();
|
|
88
|
-
throw new AuthRequiredError('Сессия истекла. Запустите инструмент
|
|
88
|
+
throw new AuthRequiredError('Сессия истекла. Запустите инструмент moab_login заново.');
|
|
89
89
|
}
|
|
90
90
|
const b = (await res.json());
|
|
91
91
|
const tokens = { accessToken: b.access_token, refreshToken: b.refresh_token, expiresAt: this.now() + b.expires_in * 1000 };
|
|
@@ -95,7 +95,7 @@ export class Authenticator {
|
|
|
95
95
|
async completePendingOrThrow(pending) {
|
|
96
96
|
if (this.now() > pending.deadline) {
|
|
97
97
|
await this.store.clear();
|
|
98
|
-
throw new AuthRequiredError('Код подтверждения истёк. Запустите
|
|
98
|
+
throw new AuthRequiredError('Код подтверждения истёк. Запустите moab_login заново.');
|
|
99
99
|
}
|
|
100
100
|
const res = await this.fetchFn(this.tokenUrl(), {
|
|
101
101
|
method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
@@ -111,7 +111,7 @@ export class Authenticator {
|
|
|
111
111
|
if (err.error === 'authorization_pending' || err.error === 'slow_down')
|
|
112
112
|
throw new AuthPendingError('Вход ещё не подтверждён. Подтвердите в браузере и повторите запрос.');
|
|
113
113
|
await this.store.clear();
|
|
114
|
-
throw new AuthRequiredError('Не удалось войти. Запустите инструмент
|
|
114
|
+
throw new AuthRequiredError('Не удалось войти. Запустите инструмент moab_login заново.');
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
/** preferred_username/email из payload JWT (без верификации — только для отображения). */
|
package/dist/errors.js
CHANGED
|
@@ -10,7 +10,7 @@ export class ApiError extends Error {
|
|
|
10
10
|
/** Дружелюбный русский текст по HTTP-коду ответа /api/recaps. */
|
|
11
11
|
export function ruRecapError(status, body) {
|
|
12
12
|
switch (status) {
|
|
13
|
-
case 401: return '✗ Вход не принят или истёк. Выполни инструмент
|
|
13
|
+
case 401: return '✗ Вход не принят или истёк. Выполни инструмент moab_login (device-flow) и повтори.';
|
|
14
14
|
case 403: return '✗ Доступ запрещён: нужна портальная роль crew. Обратись к администратору портала.';
|
|
15
15
|
case 409: return '⚠ Сервер счёл recap дубликатом (409). Если ты не отправлял его только что повторно — возможно ушло пустое тело; проверь дашборд. Памятка НЕ обновлена.';
|
|
16
16
|
case 429: return '✗ Rate limit превышен, попробуй позже.';
|
package/dist/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
function readJson(file) {
|
|
@@ -11,22 +11,86 @@ function readJson(file) {
|
|
|
11
11
|
return {};
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/** Удаляет из cfg.hooks все записи, ссылающиеся на старые recap-*.ps1 (legacy-плагин). Возвращает число удалённых. */
|
|
15
|
+
function stripLegacyHooks(cfg) {
|
|
16
|
+
if (!cfg.hooks || typeof cfg.hooks !== 'object')
|
|
17
|
+
return 0;
|
|
18
|
+
const isLegacy = (c) => typeof c === 'string' && /recap-[a-z-]*\.ps1/i.test(c);
|
|
19
|
+
let removed = 0;
|
|
20
|
+
for (const ev of Object.keys(cfg.hooks)) {
|
|
21
|
+
let arr = cfg.hooks[ev];
|
|
22
|
+
if (!Array.isArray(arr))
|
|
23
|
+
continue;
|
|
24
|
+
for (const g of arr)
|
|
25
|
+
if (g && Array.isArray(g.hooks)) {
|
|
26
|
+
const before = g.hooks.length;
|
|
27
|
+
g.hooks = g.hooks.filter((h) => !isLegacy(h && h.command));
|
|
28
|
+
removed += before - g.hooks.length;
|
|
29
|
+
}
|
|
30
|
+
arr = arr.filter((g) => {
|
|
31
|
+
if (g && Array.isArray(g.hooks))
|
|
32
|
+
return g.hooks.length > 0;
|
|
33
|
+
if (g && isLegacy(g.command)) {
|
|
34
|
+
removed++;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
if (arr.length)
|
|
40
|
+
cfg.hooks[ev] = arr;
|
|
41
|
+
else
|
|
42
|
+
delete cfg.hooks[ev];
|
|
43
|
+
}
|
|
44
|
+
return removed;
|
|
45
|
+
}
|
|
46
|
+
const CMD_MOABRECAP = `---
|
|
47
|
+
description: Сделать recap текущей работы и отправить (MoabRecap)
|
|
48
|
+
---
|
|
49
|
+
Сделай recap текущей работы и отправь его инструментом send_recap (MCP-сервер moab-recap).
|
|
50
|
+
|
|
51
|
+
Если send_recap (или list_projects) ответит «выполни moab_login» — сначала вызови инструмент moab_login, покажи пользователю ссылку и код, дождись подтверждения в браузере, затем повтори.
|
|
52
|
+
|
|
53
|
+
ОКНО: описывай только то, что сделано С МОМЕНТА ПРОШЛОГО РЕКАПА (инструмент сам посчитает окно по git и времени).
|
|
54
|
+
|
|
55
|
+
SUMMARY (поле summary) — для РУКОВОДИТЕЛЯ, НЕ для разработчика: 3-5 предложений по-русски, жёсткий потолок 1800 символов, простым языком без техники (бизнес-результат, не «refactor/endpoint/Mongo»), и ОБЯЗАТЕЛЬНО 1-2 предложения про «обратную сторону» (что не успели / где застряли / какой долг вскрылся); если реально гладко — «Открытых хвостов в этом окне нет.»
|
|
56
|
+
|
|
57
|
+
TAGS (1-3 из): feature, bugfix, refactor, docs, tests, setup, infra, research, review, planning, other.
|
|
58
|
+
COMPLEXITY (1-5): 1=опечатка / 2=мелкий CRUD / 3=многокомпонентная фича или нетривиальный дебаг / 4=многофайловый рефактор или сложная гонка / 5=архитектура.
|
|
59
|
+
EFFORT (1-5): 1<15мин / 2~30мин / 3=1-2ч / 4=полдня / 5=день+.
|
|
60
|
+
|
|
61
|
+
ПРОВЕРКА «маловато»: если complexity ≤ 3 — сначала спроси пользователя через AskUserQuestion «Маловато изменений для рекапа. Может, ещё поработаем?» (варианты: «Да, поработаем» / «Нет, сделать рекап сейчас»). Если «Да» — НЕ вызывай send_recap, заверши строкой «✓ Ок, продолжаем — recap не отправлен.» Если complexity ≥ 4 — сразу вызывай send_recap.
|
|
62
|
+
|
|
63
|
+
Если пользователь дал аргумент-подсказку ($ARGUMENTS) — учти её и передай как userHint.
|
|
64
|
+
`;
|
|
65
|
+
const CMD_SWITCH = `---
|
|
66
|
+
description: Выбрать/сменить проект, закреплённый за сессией recap (MoabRecap)
|
|
67
|
+
---
|
|
68
|
+
Смени проект, закреплённый за этой сессией (MCP-сервер moab-recap).
|
|
69
|
+
|
|
70
|
+
1. Если у сессии уже закреплён проект и это не research — сначала предложи пользователю сделать /moabrecap по текущему проекту (через AskUserQuestion). Согласен — выполни /moabrecap, потом продолжай.
|
|
71
|
+
2. Если дан аргумент-имя ($ARGUMENTS) — используй его напрямую как имя проекта. Иначе вызови инструмент list_projects (если он ответит «выполни moab_login» — сначала moab_login: покажи ссылку+код, дождись подтверждения, повтори list_projects), покажи нумерованный список (плюс «0. Research-сессия без проекта» и «N. Создать новый проект»), жди цифру.
|
|
72
|
+
3. Сопоставь цифру: 0 → pin_project с "__research__"; существующий → pin_project с этим именем; «создать новый» → спроси имя и pin_project с ним.
|
|
73
|
+
4. После закрепления подтверди и покажи готовую строку «/rename <имя>» (её пользователь скопирует сам).
|
|
74
|
+
`;
|
|
75
|
+
const CMD_LOGIN = `---
|
|
76
|
+
description: Войти в MoabRecap (device-flow, портальная роль crew)
|
|
77
|
+
---
|
|
78
|
+
Вызови инструмент moab_login (MCP-сервер moab-recap). Он вернёт ссылку и код подтверждения — покажи их пользователю и попроси открыть ссылку в браузере и подтвердить вход (нужна портальная роль crew). После подтверждения можешь проверить moab_auth_status.
|
|
79
|
+
`;
|
|
18
80
|
export async function runInit() {
|
|
19
81
|
const home = homedir();
|
|
20
|
-
const claudeJson = join(home, '.claude.json');
|
|
21
|
-
const settingsJson = join(home, '.claude', 'settings.json');
|
|
82
|
+
const claudeJson = join(home, '.claude.json');
|
|
83
|
+
const settingsJson = join(home, '.claude', 'settings.json');
|
|
84
|
+
const commandsDir = join(home, '.claude', 'commands');
|
|
22
85
|
mkdirSync(join(home, '.claude'), { recursive: true });
|
|
23
|
-
|
|
86
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
87
|
+
// 1) MCP-сервер в ~/.claude.json (литеральные non-secret env)
|
|
24
88
|
const cj = readJson(claudeJson);
|
|
25
89
|
if (existsSync(claudeJson)) {
|
|
26
90
|
try {
|
|
27
91
|
copyFileSync(claudeJson, claudeJson + '.recap-bak');
|
|
28
92
|
}
|
|
29
|
-
catch { /*
|
|
93
|
+
catch { /* ignore */ }
|
|
30
94
|
}
|
|
31
95
|
cj.mcpServers ??= {};
|
|
32
96
|
cj.mcpServers['moab-recap'] = {
|
|
@@ -40,8 +104,15 @@ export async function runInit() {
|
|
|
40
104
|
},
|
|
41
105
|
};
|
|
42
106
|
writeFileSync(claudeJson, JSON.stringify(cj, null, 2));
|
|
43
|
-
// 2)
|
|
107
|
+
// 2) settings.json: вычистить legacy recap-*.ps1 хуки + добавить наш SessionStart-хук
|
|
44
108
|
const sj = readJson(settingsJson);
|
|
109
|
+
if (existsSync(settingsJson)) {
|
|
110
|
+
try {
|
|
111
|
+
copyFileSync(settingsJson, settingsJson + '.recap-bak');
|
|
112
|
+
}
|
|
113
|
+
catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
const removedHooks = stripLegacyHooks(sj);
|
|
45
116
|
sj.hooks ??= {};
|
|
46
117
|
sj.hooks.SessionStart ??= [];
|
|
47
118
|
const already = JSON.stringify(sj.hooks.SessionStart).includes('recap-mcp');
|
|
@@ -52,9 +123,28 @@ export async function runInit() {
|
|
|
52
123
|
});
|
|
53
124
|
}
|
|
54
125
|
writeFileSync(settingsJson, JSON.stringify(sj, null, 2));
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
126
|
+
// 3) команды: отодвинуть старые одноимённые (legacy-плагин) в бэкап + записать чистые
|
|
127
|
+
const bakDir = join(commandsDir, '_pre-recap-mcp-bak');
|
|
128
|
+
let movedCmds = 0;
|
|
129
|
+
for (const legacy of ['moabrecap.md', 'switch-project.md', 'moabupdate.md']) {
|
|
130
|
+
const p = join(commandsDir, legacy);
|
|
131
|
+
if (existsSync(p)) {
|
|
132
|
+
mkdirSync(bakDir, { recursive: true });
|
|
133
|
+
try {
|
|
134
|
+
renameSync(p, join(bakDir, legacy));
|
|
135
|
+
movedCmds++;
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
writeFileSync(join(commandsDir, 'moabrecap.md'), CMD_MOABRECAP);
|
|
141
|
+
writeFileSync(join(commandsDir, 'switch-project.md'), CMD_SWITCH);
|
|
142
|
+
writeFileSync(join(commandsDir, 'moab-login.md'), CMD_LOGIN);
|
|
143
|
+
process.stdout.write('✓ recap-mcp установлен (глобально, все сессии):\n' +
|
|
144
|
+
` • MCP-сервер → ${claudeJson} (mcpServers.moab-recap)\n` +
|
|
145
|
+
` • SessionStart хук → ${settingsJson}\n` +
|
|
146
|
+
` • команды → ${commandsDir}/ (/moabrecap, /switch-project, /moab-login)\n` +
|
|
147
|
+
(removedHooks ? ` • вычищено старых recap-хуков: ${removedHooks}\n` : '') +
|
|
148
|
+
(movedCmds ? ` • старые команды отодвинуты → ${bakDir}/ (${movedCmds})\n` : '') +
|
|
149
|
+
'Перезапусти Claude Code. Первый /switch-project (или /moabrecap) попросит выполнить вход — нужна портальная роль crew.\n');
|
|
60
150
|
}
|
package/dist/server.js
CHANGED
|
@@ -2,12 +2,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { RecapApi } from './api-client.js';
|
|
3
3
|
import { FileTokenStore, Authenticator } from './auth.js';
|
|
4
4
|
import { registerTools } from './tools/index.js';
|
|
5
|
-
import { registerPrompts } from './prompts/index.js';
|
|
6
5
|
export function createServer(deps) {
|
|
7
|
-
const server = new McpServer({ name: 'moab-recap', version: '0.1.
|
|
6
|
+
const server = new McpServer({ name: 'moab-recap', version: '0.1.2' });
|
|
8
7
|
const auth = new Authenticator({ authority: deps.config.authority, client: deps.config.client }, new FileTokenStore(deps.config.configDir));
|
|
9
8
|
const api = new RecapApi(deps.config, auth);
|
|
10
9
|
registerTools(server, { config: deps.config, api, auth });
|
|
11
|
-
registerPrompts(server);
|
|
12
10
|
return server;
|
|
13
11
|
}
|
package/dist/session-start.js
CHANGED
|
@@ -37,8 +37,12 @@ export async function runSessionStart() {
|
|
|
37
37
|
ctx = `Проект «${sess.project}» закреплён за этой сессией (рекапы). /moabrecap — сделать recap, /switch-project — сменить.`;
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
ctx = '
|
|
41
|
-
+ '
|
|
40
|
+
ctx = 'ВАЖНО: эта сессия НЕ закреплена за проектом для рекапов (MoabRecap). '
|
|
41
|
+
+ 'НЕ начинай выполнять задачи пользователя, пока проект не закреплён. '
|
|
42
|
+
+ 'Сначала вызови команду /switch-project — она (при необходимости попросит выполнить инструмент moab_login) '
|
|
43
|
+
+ 'покажет список проектов и закрепит выбор за сессией: существующий проект, либо «создать новый», '
|
|
44
|
+
+ 'либо research-сессия (без рекапов). Только после закрепления переходи к задаче пользователя. '
|
|
45
|
+
+ 'Без закреплённого проекта /moabrecap не отправится.';
|
|
42
46
|
}
|
|
43
47
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: ctx } }));
|
|
44
48
|
}
|
package/dist/tools/auth-tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { run, ok } from './index.js';
|
|
2
2
|
export function registerAuthTools(server, deps) {
|
|
3
|
-
server.registerTool('
|
|
4
|
-
title: 'Войти',
|
|
3
|
+
server.registerTool('moab_login', {
|
|
4
|
+
title: 'Войти (MoabRecap)',
|
|
5
5
|
description: 'Вход в MoabRecap через auth.moab.tools (device flow). Возвращает ссылку и код для подтверждения в браузере. Нужна портальная роль crew.',
|
|
6
6
|
inputSchema: {},
|
|
7
7
|
}, async () => run(async () => {
|
|
@@ -11,20 +11,20 @@ export function registerAuthTools(server, deps) {
|
|
|
11
11
|
dc.verification_uri_complete,
|
|
12
12
|
`Код подтверждения: ${dc.user_code}`,
|
|
13
13
|
'',
|
|
14
|
-
'После подтверждения
|
|
14
|
+
'После подтверждения повторите операцию (/moabrecap или /switch-project) — вход применится автоматически.',
|
|
15
15
|
].join('\n');
|
|
16
16
|
}));
|
|
17
|
-
server.registerTool('
|
|
18
|
-
title: 'Статус входа',
|
|
17
|
+
server.registerTool('moab_auth_status', {
|
|
18
|
+
title: 'Статус входа (MoabRecap)',
|
|
19
19
|
description: 'Показать, выполнен ли вход и под каким пользователем.',
|
|
20
20
|
inputSchema: {},
|
|
21
21
|
annotations: { readOnlyHint: true },
|
|
22
22
|
}, async () => run(async () => {
|
|
23
23
|
const s = await deps.auth.status();
|
|
24
|
-
return s.loggedIn ? `Вы вошли как ${s.username ?? '(неизвестно)'}.` : 'Вход не выполнен. Запустите инструмент
|
|
24
|
+
return s.loggedIn ? `Вы вошли как ${s.username ?? '(неизвестно)'}.` : 'Вход не выполнен. Запустите инструмент moab_login.';
|
|
25
25
|
}));
|
|
26
|
-
server.registerTool('
|
|
27
|
-
title: 'Выйти',
|
|
26
|
+
server.registerTool('moab_auth_logout', {
|
|
27
|
+
title: 'Выйти (MoabRecap)',
|
|
28
28
|
description: 'Удалить сохранённые токены входа.',
|
|
29
29
|
inputSchema: {},
|
|
30
30
|
}, async () => { await deps.auth.logout(); return ok('Вы вышли. Сохранённые токены удалены.'); });
|
package/dist/tools/send-recap.js
CHANGED
|
@@ -31,7 +31,7 @@ export function registerSendRecap(server, deps) {
|
|
|
31
31
|
throw new Error('Сессия в research-режиме — recap не отправляется. /switch-project чтобы выбрать проект.');
|
|
32
32
|
if (!sess.project)
|
|
33
33
|
throw new Error('Проект для сессии не закреплён. Вызови /switch-project.');
|
|
34
|
-
await deps.auth.getAccessToken(); // бросит AuthRequiredError → run() покажет «запустите
|
|
34
|
+
await deps.auth.getAccessToken(); // бросит AuthRequiredError → run() покажет «запустите moab_login»
|
|
35
35
|
const meta = collectMeta(deps.config, cwd);
|
|
36
36
|
const tPath = latestTranscriptPath();
|
|
37
37
|
const transcript = readTranscript(tPath);
|
package/package.json
CHANGED