logwork-cli 0.0.0-stage → 1.1.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.
- package/LICENSE +11 -0
- package/README.md +330 -2
- package/RELEASE.md +89 -0
- package/bin/logwork.mjs +104 -0
- package/commands/auth.mjs +227 -0
- package/commands/commits.mjs +82 -0
- package/commands/delete.mjs +69 -0
- package/commands/doctor.mjs +24 -0
- package/commands/edit.mjs +88 -0
- package/commands/history.mjs +62 -0
- package/commands/query.mjs +52 -0
- package/commands/quick.mjs +163 -0
- package/commands/reconcile.mjs +55 -0
- package/commands/release.mjs +77 -0
- package/commands/reminder.mjs +172 -0
- package/commands/setup.mjs +508 -0
- package/commands/skill.mjs +91 -0
- package/commands/tui.mjs +50 -0
- package/commands/update.mjs +161 -0
- package/docs/advanced.md +334 -0
- package/docs/mcp-setup.md +368 -0
- package/docs/operations-guide.md +587 -0
- package/docs/recipes.md +259 -0
- package/docs/security.md +151 -0
- package/examples/mcp/antigravity.mcp_config.json +8 -0
- package/examples/mcp/codex.config.toml +5 -0
- package/examples/mcp/copilot.mcp.json +9 -0
- package/lib/agent-skill.mjs +16 -0
- package/lib/api-auth.mjs +1259 -0
- package/lib/api.mjs +1422 -0
- package/lib/apply-ledger.mjs +227 -0
- package/lib/atomic-file.mjs +24 -0
- package/lib/auth-diagnostics.mjs +113 -0
- package/lib/auth-errors.mjs +22 -0
- package/lib/auth-redaction.mjs +78 -0
- package/lib/auth-terminal.mjs +97 -0
- package/lib/auth.mjs +323 -0
- package/lib/batch-parser.mjs +145 -0
- package/lib/batch-workflow.mjs +842 -0
- package/lib/commit-workflow.mjs +402 -0
- package/lib/config.mjs +191 -0
- package/lib/credential-store.mjs +130 -0
- package/lib/diagnostics.mjs +307 -0
- package/lib/entrypoint.mjs +14 -0
- package/lib/file-lock.mjs +44 -0
- package/lib/http.mjs +125 -0
- package/lib/jira-api.mjs +258 -0
- package/lib/jira-auth.mjs +203 -0
- package/lib/jira-workflow.mjs +523 -0
- package/lib/keychain.mjs +105 -0
- package/lib/legacy-migration.mjs +66 -0
- package/lib/list-navigation.mjs +19 -0
- package/lib/logwork-config.mjs +199 -0
- package/lib/logwork-f03-enrich.mjs +41 -0
- package/lib/macos.mjs +33 -0
- package/lib/manual-apply-state.mjs +35 -0
- package/lib/manual-commands.mjs +294 -0
- package/lib/manual-drafts.mjs +154 -0
- package/lib/manual-ink-app.mjs +1654 -0
- package/lib/manual-ink-auth.mjs +349 -0
- package/lib/manual-ink-ui.mjs +675 -0
- package/lib/manual-input.mjs +21 -0
- package/lib/manual-logwork-wizard.mjs +310 -0
- package/lib/manual-mcp.mjs +187 -0
- package/lib/manual-repl.mjs +1037 -0
- package/lib/manual-targets.mjs +78 -0
- package/lib/mcp-server.mjs +833 -0
- package/lib/os-notification.mjs +105 -0
- package/lib/package-info.mjs +17 -0
- package/lib/paths.mjs +68 -0
- package/lib/project-identity.mjs +66 -0
- package/lib/project-mapping-workflow.mjs +309 -0
- package/lib/project-resolver.mjs +373 -0
- package/lib/query-workflow.mjs +553 -0
- package/lib/reconciliation-workflow.mjs +446 -0
- package/lib/release-doctor.mjs +448 -0
- package/lib/reminder-config.mjs +71 -0
- package/lib/reminder-scheduler.mjs +301 -0
- package/lib/reminder-service.mjs +118 -0
- package/lib/reminder-state.mjs +38 -0
- package/lib/reminder-workflow.mjs +299 -0
- package/lib/ro-delete-args.mjs +38 -0
- package/lib/ro-delete-workflow.mjs +115 -0
- package/lib/ro-edit-args.mjs +142 -0
- package/lib/ro-edit-workflow.mjs +632 -0
- package/lib/ro-entry-reader.mjs +56 -0
- package/lib/ro-resubmit-workflow.mjs +134 -0
- package/lib/terminal-auth-prompts.mjs +120 -0
- package/lib/ui.mjs +133 -0
- package/lib/update-service.mjs +350 -0
- package/lib/util.mjs +66 -0
- package/lib/windows-credential-manager.mjs +260 -0
- package/lib/worklog-task-resolver.mjs +113 -0
- package/package.json +73 -4
- package/setup.sh +22 -0
- package/skills/logwork/SKILL.md +43 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { addLocalDays, todayLocalDateISO } from './util.mjs';
|
|
2
|
+
import {
|
|
3
|
+
projectIdentityKey,
|
|
4
|
+
sameProjectIdentity
|
|
5
|
+
} from './project-identity.mjs';
|
|
6
|
+
|
|
7
|
+
const TASK_LINE_RE = /^\+(\d+(?:\.\d+)?)\s+(.+)$/;
|
|
8
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
9
|
+
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
10
|
+
const SHORT_WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
11
|
+
|
|
12
|
+
export function parseTaskLine(line) {
|
|
13
|
+
const match = TASK_LINE_RE.exec(String(line || '').trim());
|
|
14
|
+
if (!match) {
|
|
15
|
+
throw new Error('Task must use format: +2 Feature Development | check ui/ux');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hours = Number(match[1]);
|
|
19
|
+
if (!Number.isFinite(hours) || hours <= 0) {
|
|
20
|
+
throw new Error(`Invalid task hours: ${match[1]}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const taskName = match[2].trim();
|
|
24
|
+
if (!taskName) {
|
|
25
|
+
throw new Error('Task name is required.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
hours,
|
|
30
|
+
taskName,
|
|
31
|
+
line: `+${formatHours(hours)} ${taskName}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildLogworkText({ date, tasks }) {
|
|
36
|
+
return [
|
|
37
|
+
formatDateHeading(date),
|
|
38
|
+
...tasks.map((task) => `+${formatHours(task.hours)} ${task.taskName}`)
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildProjectOverrides({ date, tasks, projectMemberId }) {
|
|
43
|
+
return Object.fromEntries(tasks.map((_, index) => [
|
|
44
|
+
`${date}-${String(index + 1).padStart(2, '0')}`,
|
|
45
|
+
projectMemberId
|
|
46
|
+
]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildWeekDateOptions(weekly) {
|
|
50
|
+
const range = weekly?.range;
|
|
51
|
+
if (!range?.from || !range?.to) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const byDate = new Map((weekly.days || []).map((day) => [day.date, day]));
|
|
56
|
+
const options = [];
|
|
57
|
+
for (let date = range.from; date < range.to; date = addLocalDays(date, 1)) {
|
|
58
|
+
const day = byDate.get(date) || {
|
|
59
|
+
date,
|
|
60
|
+
bookedHours: 0,
|
|
61
|
+
loggedHours: 0,
|
|
62
|
+
projects: []
|
|
63
|
+
};
|
|
64
|
+
options.push({
|
|
65
|
+
date,
|
|
66
|
+
label: `${shortWeekday(date)} ${date} · ${formatHours(day.loggedHours)}h logged / ${formatHours(day.bookedHours)}h booked`,
|
|
67
|
+
bookedHours: Number(day.bookedHours || 0),
|
|
68
|
+
loggedHours: Number(day.loggedHours || 0),
|
|
69
|
+
projects: day.projects || [],
|
|
70
|
+
isToday: date === todayLocalDateISO()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return options;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildCurrentWeekDateOptions({
|
|
77
|
+
today = todayLocalDateISO()
|
|
78
|
+
} = {}) {
|
|
79
|
+
const todayDate = parseLocalDate(today);
|
|
80
|
+
const mondayOffset = todayDate.getDay() === 0 ? -6 : 1 - todayDate.getDay();
|
|
81
|
+
const monday = addLocalDays(today, mondayOffset);
|
|
82
|
+
return Array.from({ length: 7 }, (_, index) => {
|
|
83
|
+
const date = addLocalDays(monday, index);
|
|
84
|
+
return {
|
|
85
|
+
date,
|
|
86
|
+
label: `${shortWeekday(date)} ${date}`,
|
|
87
|
+
bookedHours: 0,
|
|
88
|
+
loggedHours: 0,
|
|
89
|
+
projects: [],
|
|
90
|
+
isToday: date === today
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildProjectOptions({ projectsResult, weekly, date }) {
|
|
96
|
+
const memberships = projectsResult?.projects || [];
|
|
97
|
+
const day = (weekly?.days || []).find((candidate) => candidate.date === date);
|
|
98
|
+
const bookedProjects = day?.projects || [];
|
|
99
|
+
const options = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
|
|
102
|
+
for (const project of bookedProjects) {
|
|
103
|
+
const membership = memberships.find((candidate) => sameProjectIdentity(candidate, project)) || project;
|
|
104
|
+
const option = projectOption({
|
|
105
|
+
...membership,
|
|
106
|
+
bookedHours: project.bookedHours,
|
|
107
|
+
loggedHours: project.loggedHours
|
|
108
|
+
}, { booked: true });
|
|
109
|
+
options.push(option);
|
|
110
|
+
seen.add(projectIdentityKey(option));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const project of memberships) {
|
|
114
|
+
const key = projectIdentityKey(project);
|
|
115
|
+
if (seen.has(key)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
options.push(projectOption(project, { booked: false }));
|
|
119
|
+
seen.add(key);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return options;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatDraftPreview({ target = 'ro', date, project, tasks, preview, previews }) {
|
|
126
|
+
const lines = [
|
|
127
|
+
`Date: ${date || 'not selected'}`,
|
|
128
|
+
`Target: ${target}`,
|
|
129
|
+
`Tasks: ${tasks.length} · Total: ${formatHours(totalTaskHours(tasks))}h`
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
if (target === 'ro' || target === 'both') {
|
|
133
|
+
lines.splice(2, 0, `Project: ${project?.projectName || 'not selected'}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (project && project.booked === false) {
|
|
137
|
+
lines.push('Project status: UNBOOKED for selected date');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (tasks.length) {
|
|
141
|
+
lines.push('');
|
|
142
|
+
tasks.forEach((task, index) => {
|
|
143
|
+
lines.push(`${index + 1}. +${formatHours(task.hours)} ${task.taskName}`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (preview) {
|
|
148
|
+
appendPreviewLines(lines, preview);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (previews) {
|
|
152
|
+
lines.push('');
|
|
153
|
+
if (previews.ro) {
|
|
154
|
+
lines.push(`RO preview: ${previews.ro.status}`);
|
|
155
|
+
}
|
|
156
|
+
if (previews.jira) {
|
|
157
|
+
lines.push(`Jira preview: ${previews.jira.status}`);
|
|
158
|
+
if (preview !== previews.jira) {
|
|
159
|
+
appendJiraPreviewLines(lines, previews.jira, { target });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendPreviewLines(lines, preview) {
|
|
168
|
+
lines.push('');
|
|
169
|
+
lines.push(`Preview: ${preview.status}`);
|
|
170
|
+
if (preview.errors?.length) {
|
|
171
|
+
for (const error of preview.errors) {
|
|
172
|
+
lines.push(`- Parse error: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (preview.unresolvedEntries?.length) {
|
|
176
|
+
for (const entry of preview.unresolvedEntries) {
|
|
177
|
+
lines.push(`- Unresolved: +${formatHours(entry.hours)} ${entry.taskName} (${entry.reason})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (preview.unbookedEntries?.length) {
|
|
181
|
+
lines.push(`- Unbooked entries: ${preview.unbookedEntries.length}; /apply requires extra confirmation`);
|
|
182
|
+
}
|
|
183
|
+
const jiraBlocked = (preview.entries || []).filter((entry) => entry.status && entry.status !== 'ready' && !entry.matchedProject);
|
|
184
|
+
if (jiraBlocked.length) {
|
|
185
|
+
lines.push(`- Blocked Jira entries: ${jiraBlocked.length}`);
|
|
186
|
+
}
|
|
187
|
+
if (preview.totals?.readyCount !== undefined) {
|
|
188
|
+
appendJiraPreviewLines(lines, preview);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function appendJiraPreviewLines(lines, preview, { target = 'jira' } = {}) {
|
|
193
|
+
const totals = preview.totals || {};
|
|
194
|
+
lines.push(`- Jira counts: ready ${totals.readyCount || 0}, duplicate ${totals.duplicateCount || 0}, missing ticket ${totals.missingIssueCount || 0}, multiple tickets ${totals.multipleIssueCount || 0}, lookup failed ${totals.lookupFailedCount || 0}`);
|
|
195
|
+
const missing = (preview.entries || []).filter((entry) => entry.reason === 'missing_issue_key');
|
|
196
|
+
if (missing.length) {
|
|
197
|
+
lines.push(`- Warning: ${missing.length} task${missing.length === 1 ? '' : 's'} missing a Jira ticket${target === 'both' ? '; Both apply is blocked' : ''}.`);
|
|
198
|
+
}
|
|
199
|
+
for (const entry of preview.entries || []) {
|
|
200
|
+
if (entry.issue) {
|
|
201
|
+
lines.push(`- ${entry.issueKey}: ${entry.issue.summary || 'No summary'} [${entry.issue.status || 'unknown status'}]`);
|
|
202
|
+
}
|
|
203
|
+
if (entry.worklog?.comment) {
|
|
204
|
+
lines.push(` Comment: ${String(entry.worklog.comment).replace(/\s*\n\s*/g, ' | ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function totalTaskHours(tasks) {
|
|
210
|
+
return tasks.reduce((sum, task) => sum + Number(task.hours || 0), 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function toggleTaskSelection(selectedIndexes, index) {
|
|
214
|
+
const selected = new Set([...selectedIndexes].map(Number));
|
|
215
|
+
if (selected.has(index)) {
|
|
216
|
+
selected.delete(index);
|
|
217
|
+
} else {
|
|
218
|
+
selected.add(index);
|
|
219
|
+
}
|
|
220
|
+
return [...selected].sort((left, right) => left - right);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function removeDraftTasks(tasks, selectedIndexes) {
|
|
224
|
+
const selected = new Set([...selectedIndexes].map(Number));
|
|
225
|
+
return tasks.filter((_, index) => !selected.has(index));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function replaceDraftTask(tasks, index, task) {
|
|
229
|
+
return tasks.map((currentTask, currentIndex) => currentIndex === index ? task : currentTask);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function canApplyPreview(preview, {
|
|
233
|
+
target = 'ro'
|
|
234
|
+
} = {}) {
|
|
235
|
+
if (!preview) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
reason: 'Build a preview before applying.'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (preview.errors?.length) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
reason: 'Cannot apply preview with parse errors.'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (target === 'jira') {
|
|
250
|
+
const blocked = (preview.entries || []).filter((entry) => entry.status !== 'ready');
|
|
251
|
+
if (blocked.length) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
reason: `Cannot apply Jira preview with ${blocked.length} blocked entries.`
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (!(preview.entries || []).length) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
reason: 'Cannot apply Jira preview with no entries.'
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (preview.unresolvedEntries?.length) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
reason: `Cannot apply preview with ${preview.unresolvedEntries.length} unresolved entries.`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
reason: ''
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatDateHeading(localDateISO) {
|
|
279
|
+
const date = parseLocalDate(localDateISO);
|
|
280
|
+
return `${WEEKDAYS[date.getDay()]}, ${String(date.getDate()).padStart(2, '0')} ${MONTHS[date.getMonth()]} ${date.getFullYear()}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function formatHours(value) {
|
|
284
|
+
const number = Number(value || 0);
|
|
285
|
+
return Number.isInteger(number) ? String(number) : String(Number(number.toFixed(2))).replace(/\.0$/, '');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function projectOption(project, { booked }) {
|
|
289
|
+
const bookedText = booked
|
|
290
|
+
? `${formatHours(project.loggedHours)}h logged / ${formatHours(project.bookedHours)}h booked`
|
|
291
|
+
: 'UNBOOKED';
|
|
292
|
+
return {
|
|
293
|
+
projectMemberId: project.projectMemberId,
|
|
294
|
+
projectId: project.projectId,
|
|
295
|
+
projectName: project.projectName,
|
|
296
|
+
booked,
|
|
297
|
+
bookedHours: project.bookedHours,
|
|
298
|
+
loggedHours: project.loggedHours,
|
|
299
|
+
label: `${project.projectName} · memberId ${project.projectMemberId ?? 'unknown'} · ${bookedText}`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function shortWeekday(localDateISO) {
|
|
304
|
+
return SHORT_WEEKDAYS[parseLocalDate(localDateISO).getDay()] || localDateISO;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseLocalDate(localDateISO) {
|
|
308
|
+
const [year, month, day] = String(localDateISO).split('-').map(Number);
|
|
309
|
+
return new Date(year, month - 1, day);
|
|
310
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { helperHome } from './paths.mjs';
|
|
3
|
+
|
|
4
|
+
export const MCP_CLIENTS = [
|
|
5
|
+
{
|
|
6
|
+
key: 'cursor',
|
|
7
|
+
label: 'Cursor',
|
|
8
|
+
aliases: ['cursor'],
|
|
9
|
+
target: 'Cursor MCP settings',
|
|
10
|
+
format: 'JSON'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: 'antigravity',
|
|
14
|
+
label: 'Google Antigravity',
|
|
15
|
+
aliases: ['antigravity', 'google-antigravity'],
|
|
16
|
+
target: '~/.gemini/antigravity/mcp_config.json',
|
|
17
|
+
format: 'JSON'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: 'copilot',
|
|
21
|
+
label: 'GitHub Copilot / VS Code',
|
|
22
|
+
aliases: ['copilot', 'github-copilot', 'vscode', 'vs-code'],
|
|
23
|
+
target: 'workspace .vscode/mcp.json or VS Code user MCP config',
|
|
24
|
+
format: 'JSON'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'claude-code',
|
|
28
|
+
label: 'Claude Code',
|
|
29
|
+
aliases: ['claude', 'claude-code'],
|
|
30
|
+
target: 'claude mcp add or project .mcp.json',
|
|
31
|
+
format: 'CLI or JSON'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'codex',
|
|
35
|
+
label: 'Codex',
|
|
36
|
+
aliases: ['codex'],
|
|
37
|
+
target: '~/.codex/config.toml or project .codex/config.toml',
|
|
38
|
+
format: 'TOML'
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// IDE configs run `node <runtime>/bin/logwork.mjs mcp`. An absolute node entry
|
|
43
|
+
// path is used instead of the `logwork` command because GUI IDEs often start
|
|
44
|
+
// without the npm global bin directory on PATH.
|
|
45
|
+
export const MCP_SERVER_NAME = 'logwork';
|
|
46
|
+
|
|
47
|
+
export function defaultMcpServerPath() {
|
|
48
|
+
return join(helperHome(), 'bin', 'logwork.mjs');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveMcpClient(value) {
|
|
52
|
+
const normalized = normalizeMcpClientValue(value);
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return MCP_CLIENTS.find((client) => (
|
|
58
|
+
client.key === normalized ||
|
|
59
|
+
client.aliases.includes(normalized)
|
|
60
|
+
)) || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveMcpClientSelection(value) {
|
|
64
|
+
const trimmed = String(value || '').trim();
|
|
65
|
+
const numeric = Number(trimmed);
|
|
66
|
+
if (/^\d+$/.test(trimmed) && Number.isInteger(numeric)) {
|
|
67
|
+
return MCP_CLIENTS[numeric - 1] || null;
|
|
68
|
+
}
|
|
69
|
+
return resolveMcpClient(trimmed);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatMcpClientOptions(clients = MCP_CLIENTS) {
|
|
73
|
+
return [
|
|
74
|
+
'Choose MCP client:',
|
|
75
|
+
...clients.map((client, index) => (
|
|
76
|
+
` ${index + 1}. ${client.label} - ${client.target}`
|
|
77
|
+
)),
|
|
78
|
+
'',
|
|
79
|
+
'Type a number or name, for example: cursor, antigravity, copilot, claude-code, codex.'
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatMcpSetup({
|
|
84
|
+
client,
|
|
85
|
+
serverPath = defaultMcpServerPath(),
|
|
86
|
+
platform = process.platform
|
|
87
|
+
} = {}) {
|
|
88
|
+
const resolvedClient = typeof client === 'object' ? client : resolveMcpClient(client);
|
|
89
|
+
if (!resolvedClient) {
|
|
90
|
+
throw new Error('Unknown MCP client. Use: cursor, antigravity, copilot, claude-code, or codex.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = [
|
|
94
|
+
`MCP setup: ${resolvedClient.label}`,
|
|
95
|
+
`Entry path: ${serverPath} (run with the "mcp" argument)`,
|
|
96
|
+
''
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
if (resolvedClient.key === 'codex') {
|
|
100
|
+
lines.push(
|
|
101
|
+
`Copy this TOML into ${resolvedClient.target}:`,
|
|
102
|
+
'',
|
|
103
|
+
formatCodexToml(serverPath)
|
|
104
|
+
);
|
|
105
|
+
} else if (resolvedClient.key === 'copilot') {
|
|
106
|
+
lines.push(
|
|
107
|
+
`Copy this JSON into ${resolvedClient.target}:`,
|
|
108
|
+
'',
|
|
109
|
+
JSON.stringify(vscodeMcpConfig(serverPath), null, 2)
|
|
110
|
+
);
|
|
111
|
+
} else if (resolvedClient.key === 'claude-code') {
|
|
112
|
+
lines.push(
|
|
113
|
+
'Recommended command:',
|
|
114
|
+
'',
|
|
115
|
+
`claude mcp add --transport stdio ${MCP_SERVER_NAME} -- node ${quoteShellArg(serverPath, { platform })} mcp`,
|
|
116
|
+
'',
|
|
117
|
+
'Or copy this JSON into project .mcp.json:',
|
|
118
|
+
'',
|
|
119
|
+
JSON.stringify(stdioMcpServersConfig(serverPath), null, 2)
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(
|
|
123
|
+
`Copy this JSON into ${resolvedClient.target}:`,
|
|
124
|
+
'',
|
|
125
|
+
JSON.stringify(stdioMcpServersConfig(serverPath), null, 2)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
lines.push(
|
|
130
|
+
'',
|
|
131
|
+
'After saving, restart or reload the MCP client.',
|
|
132
|
+
'Do not paste passwords, 2FA codes, Bearer tokens, cookies, or raw auth logs into AI chat.'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeMcpClientValue(value) {
|
|
139
|
+
return String(value || '')
|
|
140
|
+
.trim()
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.replace(/[\s_]+/g, '-');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function stdioMcpServersConfig(serverPath) {
|
|
146
|
+
return {
|
|
147
|
+
mcpServers: {
|
|
148
|
+
[MCP_SERVER_NAME]: {
|
|
149
|
+
command: 'node',
|
|
150
|
+
args: [serverPath, 'mcp']
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function vscodeMcpConfig(serverPath) {
|
|
157
|
+
return {
|
|
158
|
+
servers: {
|
|
159
|
+
[MCP_SERVER_NAME]: {
|
|
160
|
+
type: 'stdio',
|
|
161
|
+
command: 'node',
|
|
162
|
+
args: [serverPath, 'mcp']
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function formatCodexToml(serverPath) {
|
|
169
|
+
return [
|
|
170
|
+
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
171
|
+
'command = "node"',
|
|
172
|
+
`args = ["${escapeTomlString(serverPath)}", "mcp"]`,
|
|
173
|
+
'startup_timeout_sec = 20',
|
|
174
|
+
'tool_timeout_sec = 120'
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function escapeTomlString(value) {
|
|
179
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function quoteShellArg(value, { platform = process.platform } = {}) {
|
|
183
|
+
if (platform === 'win32') {
|
|
184
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
185
|
+
}
|
|
186
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
187
|
+
}
|