logwork-cli 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -25
- package/RELEASE.md +4 -2
- package/bin/logwork.mjs +2 -2
- package/commands/commits.mjs +128 -13
- package/commands/history.mjs +0 -4
- package/commands/mcp.mjs +266 -0
- package/commands/quick.mjs +38 -23
- package/commands/reminder.mjs +41 -2
- package/commands/setup.mjs +197 -42
- package/commands/tui.mjs +1 -0
- package/docs/advanced.md +18 -7
- package/docs/mcp-setup.md +120 -26
- package/docs/operations-guide.md +50 -2
- package/docs/recipes.md +40 -0
- package/docs/security.md +23 -2
- package/examples/mcp/antigravity.mcp_config.json +2 -2
- package/examples/mcp/codex.config.toml +2 -2
- package/examples/mcp/copilot.mcp.json +2 -2
- package/lib/api-auth-forms.mjs +393 -0
- package/lib/api-auth-http.mjs +184 -0
- package/lib/api-auth-support.mjs +101 -0
- package/lib/api-auth-tokens.mjs +129 -0
- package/lib/api-auth.mjs +39 -813
- package/lib/api.mjs +38 -1422
- package/lib/apply-ledger.mjs +1 -1
- package/lib/batch-workflow.mjs +187 -2
- package/lib/commit-workflow.mjs +345 -23
- package/lib/config.mjs +7 -0
- package/lib/diagnostics.mjs +116 -4
- package/lib/jira-api.mjs +25 -3
- package/lib/jira-workflow.mjs +36 -6
- package/lib/launcher.mjs +303 -0
- package/lib/logwork-config.mjs +64 -13
- package/lib/manual-apply-state.mjs +15 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +142 -1282
- package/lib/manual-ink-draft-actions.mjs +227 -0
- package/lib/manual-ink-interaction.mjs +318 -0
- package/lib/manual-ink-session.mjs +41 -0
- package/lib/manual-ink-task-editor.mjs +338 -0
- package/lib/manual-ink-ui.mjs +38 -0
- package/lib/manual-ink-wizard.mjs +520 -0
- package/lib/manual-mcp.mjs +78 -20
- package/lib/manual-repl.mjs +171 -22
- package/lib/mcp-install.mjs +930 -0
- package/lib/mcp-server.mjs +51 -20
- package/lib/node-runtime.mjs +155 -0
- package/lib/paths.mjs +7 -0
- package/lib/project-mapping-workflow.mjs +32 -27
- package/lib/query-workflow.mjs +19 -1
- package/lib/reconciliation-workflow.mjs +1 -1
- package/lib/reminder-config.mjs +38 -2
- package/lib/reminder-scheduler.mjs +85 -18
- package/lib/reminder-service.mjs +42 -3
- package/lib/reminder-workflow.mjs +118 -22
- package/lib/repo-project-suggestions.mjs +285 -0
- package/lib/ro-api-diagnostics.mjs +109 -0
- package/lib/ro-api-entries.mjs +359 -0
- package/lib/ro-api-fields.mjs +290 -0
- package/lib/ro-api-http.mjs +68 -0
- package/lib/ro-api-projects.mjs +137 -0
- package/lib/ro-api-tasks.mjs +50 -0
- package/lib/ro-api-timesheet.mjs +413 -0
- package/lib/ro-delete-workflow.mjs +2 -1
- package/lib/ro-entry-reader.mjs +11 -6
- package/lib/ro-resubmit-workflow.mjs +2 -1
- package/lib/terminal-auth-prompts.mjs +7 -4
- package/lib/ui.mjs +121 -56
- package/package.json +11 -4
- package/skills/logwork/SKILL.md +10 -4
package/commands/quick.mjs
CHANGED
|
@@ -10,41 +10,56 @@ import {
|
|
|
10
10
|
import * as ui from '../lib/ui.mjs';
|
|
11
11
|
import { todayLocalDateISO } from '../lib/util.mjs';
|
|
12
12
|
|
|
13
|
+
const QUICK_API = { addLogtime, fetchWorklogTasksForProject, getTodayTimesheetProjects };
|
|
14
|
+
|
|
13
15
|
// `logwork quick [-m "what you did"]`: log one Resource Optimiser entry for
|
|
14
16
|
// today with guided prompts (project -> worklog task -> description -> hours).
|
|
15
|
-
|
|
17
|
+
// The second argument of run and the options of runQuickLog are test seams;
|
|
18
|
+
// the CLI uses the defaults.
|
|
19
|
+
export async function run(argv = [], {
|
|
20
|
+
quickLog = runQuickLog,
|
|
21
|
+
quickUi = ui,
|
|
22
|
+
showIntro = intro,
|
|
23
|
+
print = console.log
|
|
24
|
+
} = {}) {
|
|
16
25
|
const { message, help } = parseQuickArgs(argv);
|
|
17
26
|
if (help) {
|
|
18
|
-
|
|
27
|
+
print(formatQuickHelp());
|
|
19
28
|
return 0;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
try {
|
|
23
|
-
|
|
24
|
-
const result = await
|
|
25
|
-
|
|
32
|
+
showIntro('Logwork quick');
|
|
33
|
+
const result = await quickLog({ message });
|
|
34
|
+
quickUi.showSuccess(result.dryRun
|
|
26
35
|
? 'DRY RUN: payload generated; no API write performed.'
|
|
27
36
|
: 'Logwork submitted.');
|
|
28
37
|
return 0;
|
|
29
38
|
} catch (error) {
|
|
30
|
-
|
|
39
|
+
quickUi.showError(error);
|
|
31
40
|
return 1;
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
export async function runQuickLog({
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
export async function runQuickLog({
|
|
45
|
+
message = '',
|
|
46
|
+
quickUi = ui,
|
|
47
|
+
openSession: open = openSession,
|
|
48
|
+
api = QUICK_API,
|
|
49
|
+
today = todayLocalDateISO
|
|
50
|
+
} = {}) {
|
|
51
|
+
const localDateISO = today();
|
|
52
|
+
let session = await open();
|
|
38
53
|
|
|
39
54
|
let projects;
|
|
40
55
|
try {
|
|
41
|
-
projects = await loadBookedProjects(session, localDateISO);
|
|
56
|
+
projects = await loadBookedProjects(session, localDateISO, api);
|
|
42
57
|
} catch (error) {
|
|
43
58
|
if (!isUnauthorized(error)) {
|
|
44
59
|
throw error;
|
|
45
60
|
}
|
|
46
|
-
session = await
|
|
47
|
-
projects = await loadBookedProjects(session, localDateISO);
|
|
61
|
+
session = await open({ forceLogin: true });
|
|
62
|
+
projects = await loadBookedProjects(session, localDateISO, api);
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
const daySummaries = projects.map((project) => ({
|
|
@@ -52,8 +67,8 @@ export async function runQuickLog({ message = '' } = {}) {
|
|
|
52
67
|
totalHours: project.loggedHours,
|
|
53
68
|
ok: true
|
|
54
69
|
}));
|
|
55
|
-
const project = await
|
|
56
|
-
const tasks = (await fetchWorklogTasksForProject(session.token, project.projectId)).map((task) => ({
|
|
70
|
+
const project = await quickUi.chooseProject(projects, daySummaries);
|
|
71
|
+
const tasks = (await api.fetchWorklogTasksForProject(session.token, project.projectId)).map((task) => ({
|
|
57
72
|
taskId: task.id,
|
|
58
73
|
name: task.name,
|
|
59
74
|
typeOfWork: task.typeOfWork || 'other',
|
|
@@ -63,11 +78,11 @@ export async function runQuickLog({ message = '' } = {}) {
|
|
|
63
78
|
if (!tasks.length) {
|
|
64
79
|
throw new Error(`No worklog tasks are available for ${project.projectName}.`);
|
|
65
80
|
}
|
|
66
|
-
const task = await
|
|
67
|
-
const description = await
|
|
68
|
-
const hours = await
|
|
81
|
+
const task = await quickUi.chooseTask(tasks);
|
|
82
|
+
const description = await quickUi.chooseMessageSource(message);
|
|
83
|
+
const hours = await quickUi.askHours(CONFIG.defaultHours);
|
|
69
84
|
|
|
70
|
-
await
|
|
85
|
+
await quickUi.confirmSubmit({
|
|
71
86
|
projectName: project.projectName,
|
|
72
87
|
hours,
|
|
73
88
|
taskName: task.name,
|
|
@@ -88,13 +103,13 @@ export async function runQuickLog({ message = '' } = {}) {
|
|
|
88
103
|
};
|
|
89
104
|
|
|
90
105
|
try {
|
|
91
|
-
return await addLogtime(session.token, entry);
|
|
106
|
+
return await api.addLogtime(session.token, entry);
|
|
92
107
|
} catch (error) {
|
|
93
108
|
if (!isUnauthorized(error)) {
|
|
94
109
|
throw error;
|
|
95
110
|
}
|
|
96
|
-
session = await
|
|
97
|
-
return addLogtime(session.token, entry);
|
|
111
|
+
session = await open({ forceLogin: true });
|
|
112
|
+
return api.addLogtime(session.token, entry);
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
|
|
@@ -150,8 +165,8 @@ async function openSession({ forceLogin = false } = {}) {
|
|
|
150
165
|
return { token, userId: getUserIdFromJwt(token) };
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
async function loadBookedProjects(session, localDateISO) {
|
|
154
|
-
const projects = await getTodayTimesheetProjects(session.token, session.userId, localDateISO);
|
|
168
|
+
async function loadBookedProjects(session, localDateISO, api) {
|
|
169
|
+
const projects = await api.getTodayTimesheetProjects(session.token, session.userId, localDateISO);
|
|
155
170
|
if (!projects.length) {
|
|
156
171
|
throw new Error(`You have no booked project on ${localDateISO}.`);
|
|
157
172
|
}
|
package/commands/reminder.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { confirm, isCancel } from '@clack/prompts';
|
|
2
2
|
import {
|
|
3
|
+
normalizeReminderRepos,
|
|
3
4
|
normalizeReminderTime,
|
|
4
5
|
REMINDER_TARGETS
|
|
5
6
|
} from '../lib/reminder-config.mjs';
|
|
@@ -27,6 +28,7 @@ export function parseReminderArgs(args = []) {
|
|
|
27
28
|
yes: false,
|
|
28
29
|
json: false,
|
|
29
30
|
home: undefined,
|
|
31
|
+
repos: undefined,
|
|
30
32
|
help: false
|
|
31
33
|
};
|
|
32
34
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -44,6 +46,27 @@ export function parseReminderArgs(args = []) {
|
|
|
44
46
|
index += 1;
|
|
45
47
|
continue;
|
|
46
48
|
}
|
|
49
|
+
if (arg === '--repo' || arg.startsWith('--repo=')) {
|
|
50
|
+
const value = arg === '--repo' ? rest[index + 1] : arg.slice('--repo='.length);
|
|
51
|
+
if (!value || (arg === '--repo' && value.startsWith('--'))) {
|
|
52
|
+
throw new Error('--repo requires a path.');
|
|
53
|
+
}
|
|
54
|
+
if (options.repos === null) {
|
|
55
|
+
throw new Error('--repo cannot be combined with --no-repos.');
|
|
56
|
+
}
|
|
57
|
+
options.repos = [...(options.repos || []), value];
|
|
58
|
+
if (arg === '--repo') {
|
|
59
|
+
index += 1;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === '--no-repos') {
|
|
64
|
+
if (options.repos?.length) {
|
|
65
|
+
throw new Error('--repo cannot be combined with --no-repos.');
|
|
66
|
+
}
|
|
67
|
+
options.repos = null;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
47
70
|
if (arg === '--yes' || arg === '-y') {
|
|
48
71
|
options.yes = true;
|
|
49
72
|
continue;
|
|
@@ -61,6 +84,16 @@ export function parseReminderArgs(args = []) {
|
|
|
61
84
|
if (command !== 'enable' && options.time) {
|
|
62
85
|
throw new Error('--time is only valid with reminder enable.');
|
|
63
86
|
}
|
|
87
|
+
if (command !== 'enable' && options.repos !== undefined) {
|
|
88
|
+
throw new Error('--repo and --no-repos are only valid with reminder enable.');
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(options.repos)) {
|
|
91
|
+
// Resolve against the directory the command runs in; the scheduled run
|
|
92
|
+
// starts in the helper home.
|
|
93
|
+
options.repos = normalizeReminderRepos(options.repos);
|
|
94
|
+
} else if (options.repos === null) {
|
|
95
|
+
options.repos = [];
|
|
96
|
+
}
|
|
64
97
|
if (command !== 'run' && options.home) {
|
|
65
98
|
throw new Error('--home is reserved for the OS reminder runner.');
|
|
66
99
|
}
|
|
@@ -131,6 +164,7 @@ export async function runReminderCli(args = [], {
|
|
|
131
164
|
result = await enable({
|
|
132
165
|
time: options.time,
|
|
133
166
|
target: options.target,
|
|
167
|
+
...(options.repos !== undefined ? { repos: options.repos } : {}),
|
|
134
168
|
confirm: true
|
|
135
169
|
});
|
|
136
170
|
} else if (options.command === 'disable') {
|
|
@@ -147,12 +181,17 @@ export async function runReminderCli(args = [], {
|
|
|
147
181
|
|
|
148
182
|
export function formatReminderHelp() {
|
|
149
183
|
return `Usage:
|
|
150
|
-
logwork reminder enable [--time HH:mm] [--target ro|jira|both]
|
|
184
|
+
logwork reminder enable [--time HH:mm] [--target ro|jira|both]
|
|
185
|
+
[--repo <path>]... [--no-repos] [--yes]
|
|
151
186
|
logwork reminder status [--json]
|
|
152
187
|
logwork reminder test [--target ro|jira|both] [--yes]
|
|
153
188
|
logwork reminder disable [--yes]
|
|
154
189
|
|
|
155
|
-
The reminder runs Monday-Friday at 17:00 by default. It uses launchd on macOS and Task Scheduler on Windows. RO compares logged and booked hours; Jira checks whether the current user has a worklog today
|
|
190
|
+
The reminder runs Monday-Friday at 17:00 by default. It uses launchd on macOS and Task Scheduler on Windows. RO compares logged and booked hours; Jira checks whether the current user has a worklog today.
|
|
191
|
+
|
|
192
|
+
--repo (repeatable) sets the git repos whose commits today are counted when RO hours are missing, for example "Hôm nay: 5 commit trong 2 repo", and points to \`logwork commits today\` for a draft. Repos listed on project mappings are also used. --repo replaces the saved list; --no-repos clears it; without either the saved list is kept.
|
|
193
|
+
|
|
194
|
+
The schedule stores the absolute path of the Node that ran \`reminder enable\`. After upgrading or removing that Node (nvm, Homebrew), run \`logwork reminder enable\` or \`logwork setup\` again; \`logwork reminder status\` warns when the path is gone.`;
|
|
156
195
|
}
|
|
157
196
|
|
|
158
197
|
async function requireInteractiveConfirmation({ isTTY, promptConfirm, message }) {
|
package/commands/setup.mjs
CHANGED
|
@@ -5,16 +5,22 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { redactText } from '../lib/auth-redaction.mjs';
|
|
6
6
|
import { credentialStoreLabel } from '../lib/credential-store.mjs';
|
|
7
7
|
import { npmInvocation } from '../lib/npm-command.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { writeRuntimeInfo } from '../lib/node-runtime.mjs';
|
|
9
|
+
import { RUNTIME_INFO_FILE_NAME, helperHome } from '../lib/paths.mjs';
|
|
10
|
+
import { mcpArgs, resolveLaunchCommand, writeLauncher } from '../lib/launcher.mjs';
|
|
9
11
|
import {
|
|
12
|
+
MCP_LAUNCHER_NOTE,
|
|
13
|
+
MCP_NODE_PATH_NOTE,
|
|
10
14
|
MCP_SERVER_NAME,
|
|
11
|
-
|
|
15
|
+
formatClaudeMcpAddCommand,
|
|
12
16
|
formatCodexToml,
|
|
13
17
|
stdioMcpServersConfig,
|
|
14
18
|
vscodeMcpConfig
|
|
15
19
|
} from '../lib/manual-mcp.mjs';
|
|
20
|
+
import { createMcpInstallContext, describeMcpPlan, planMcpInstall } from '../lib/mcp-install.mjs';
|
|
16
21
|
import { loadReminderConfig } from '../lib/reminder-config.mjs';
|
|
17
22
|
import { installReminderScheduler } from '../lib/reminder-scheduler.mjs';
|
|
23
|
+
import { formatMcpInstallResults, isInteractiveTerminal, runMcpInstall } from './mcp.mjs';
|
|
18
24
|
|
|
19
25
|
// Package name before the rename. Its global install provides a `logwork` bin
|
|
20
26
|
// that would block `npm link` of logwork-cli, so setup removes it first.
|
|
@@ -27,8 +33,9 @@ const EXCLUDED_DIRS = new Set([
|
|
|
27
33
|
]);
|
|
28
34
|
|
|
29
35
|
// `logwork setup`: copy this package into ~/.logwork-cli, install its
|
|
30
|
-
// dependencies, link the `logwork` command,
|
|
31
|
-
//
|
|
36
|
+
// dependencies, link the `logwork` command, write the launcher, refresh a
|
|
37
|
+
// migrated reminder, and add or update the `logwork` entry in the MCP clients
|
|
38
|
+
// found on this machine.
|
|
32
39
|
export async function run(argv = []) {
|
|
33
40
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
34
41
|
console.log(formatSetupHelp());
|
|
@@ -43,16 +50,19 @@ export async function run(argv = []) {
|
|
|
43
50
|
|
|
44
51
|
export function formatSetupHelp() {
|
|
45
52
|
return `Usage:
|
|
46
|
-
logwork setup [--login|--no-login]
|
|
53
|
+
logwork setup [--yes] [--no-mcp-install] [--login|--no-login]
|
|
47
54
|
|
|
48
|
-
Installs the runtime to ~/.logwork-cli, links the \`logwork\` command, and
|
|
49
|
-
MCP
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
Installs the runtime to ~/.logwork-cli, links the \`logwork\` command, and adds
|
|
56
|
+
the "logwork" MCP server to the clients it finds (Claude Code, Cursor, Codex,
|
|
57
|
+
VS Code, Google Antigravity), asking once per client. Each edited config is
|
|
58
|
+
backed up first. Settings, mappings and the login session from an older
|
|
59
|
+
logwork-helper install are carried over automatically.
|
|
52
60
|
|
|
53
61
|
Options:
|
|
54
|
-
--
|
|
55
|
-
--no-
|
|
62
|
+
--yes, -y Update every detected MCP client without asking
|
|
63
|
+
--no-mcp-install Do not touch MCP client configs
|
|
64
|
+
--login Log in to Resource Optimiser after installing
|
|
65
|
+
--no-login Do not ask to log in`;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export async function installUserRuntime({
|
|
@@ -65,7 +75,10 @@ export async function installUserRuntime({
|
|
|
65
75
|
installFn = installDependencies,
|
|
66
76
|
linkFn = linkGlobalBinaries,
|
|
67
77
|
authFn = maybeRunAuthLogin,
|
|
78
|
+
runtimeInfoFn = recordRuntimeInfo,
|
|
79
|
+
launcherFn = writeRuntimeLauncher,
|
|
68
80
|
reminderFn = refreshReminderSchedule,
|
|
81
|
+
mcpInstallFn = installMcpClients,
|
|
69
82
|
printFn = printMcpInstructions
|
|
70
83
|
}) {
|
|
71
84
|
await mkdirFn(targetDir, { recursive: true });
|
|
@@ -76,29 +89,98 @@ export async function installUserRuntime({
|
|
|
76
89
|
await installFn(targetDir);
|
|
77
90
|
const linkResult = await linkFn(targetDir);
|
|
78
91
|
const authResult = await authFn(targetDir, options);
|
|
92
|
+
await runtimeInfoFn(targetDir);
|
|
93
|
+
await launcherFn(targetDir);
|
|
79
94
|
const reminderResult = await reminderFn(targetDir);
|
|
80
|
-
await
|
|
95
|
+
const mcpResult = await mcpInstallFn(targetDir, options);
|
|
96
|
+
await printFn(targetDir, authResult, linkResult, cleanupResult, reminderResult, mcpResult);
|
|
81
97
|
|
|
82
98
|
return {
|
|
83
99
|
targetDir,
|
|
84
100
|
authResult,
|
|
85
101
|
linkResult,
|
|
86
102
|
cleanupResult,
|
|
87
|
-
reminderResult
|
|
103
|
+
reminderResult,
|
|
104
|
+
mcpResult
|
|
88
105
|
};
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
// macOS/Linux: <home>/bin/logwork-launch, which MCP configs and the reminder
|
|
109
|
+
// run. It must exist before the reminder is re-registered.
|
|
110
|
+
export async function writeRuntimeLauncher(targetDir, {
|
|
111
|
+
nodePath = process.execPath,
|
|
112
|
+
platform = process.platform,
|
|
113
|
+
writeFn = writeLauncher
|
|
114
|
+
} = {}) {
|
|
115
|
+
return writeFn({ home: targetDir, nodePath, platform });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Adds or updates the `logwork` entry in the detected MCP clients:
|
|
119
|
+
// - --no-mcp-install: skipped;
|
|
120
|
+
// - --yes: every detected client, without asking;
|
|
121
|
+
// - a terminal: one yes/no per client that needs a change;
|
|
122
|
+
// - otherwise (npm exec from `logwork update`, CI): only the plan.
|
|
123
|
+
export async function installMcpClients(targetDir, options = {}, {
|
|
124
|
+
interactive = isInteractiveTerminal(),
|
|
125
|
+
ctx = createMcpInstallContext({ home: targetDir }),
|
|
126
|
+
install = runMcpInstall,
|
|
127
|
+
plan = planMcpInstall
|
|
128
|
+
} = {}) {
|
|
129
|
+
if (options.mcpInstall === false) {
|
|
130
|
+
return { mode: 'skipped', launch: ctx.launch };
|
|
131
|
+
}
|
|
95
132
|
try {
|
|
96
|
-
|
|
133
|
+
if (options.yes || interactive) {
|
|
134
|
+
const result = await install({ ctx, yes: Boolean(options.yes), interactive });
|
|
135
|
+
return { mode: 'installed', launch: ctx.launch, results: result.results };
|
|
136
|
+
}
|
|
137
|
+
const plans = await plan(ctx);
|
|
138
|
+
return { mode: 'planned', launch: ctx.launch, plans, lines: plans.flatMap((item) => describeMcpPlan(item, ctx)) };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return { mode: 'error', launch: ctx.launch, error: error.message };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Records the node that ran setup. The macOS/Linux launcher tries it first;
|
|
145
|
+
// on Windows the MCP configs and the reminder run it (or the nvm-windows
|
|
146
|
+
// symlink). doctor and reminder status compare it with the current Node.
|
|
147
|
+
// Best effort: a failed write must not fail the install.
|
|
148
|
+
export async function recordRuntimeInfo(targetDir, {
|
|
149
|
+
nodePath = process.execPath,
|
|
150
|
+
nodeVersion = process.version,
|
|
151
|
+
writeFn = writeRuntimeInfo,
|
|
152
|
+
readFileFn = fs.readFile
|
|
153
|
+
} = {}) {
|
|
154
|
+
try {
|
|
155
|
+
const manifest = JSON.parse(await readFileFn(join(targetDir, 'package.json'), 'utf8'));
|
|
156
|
+
return await writeFn({
|
|
157
|
+
path: join(targetDir, RUNTIME_INFO_FILE_NAME),
|
|
158
|
+
nodePath,
|
|
159
|
+
nodeVersion,
|
|
160
|
+
version: manifest.version || null
|
|
161
|
+
});
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A reminder enabled by an older install still points at the old runtime, or
|
|
168
|
+
// at a Node that nvm or Homebrew removed. Re-register it so the OS scheduler
|
|
169
|
+
// runs this runtime with the current node (and the legacy entry is removed by
|
|
170
|
+
// installReminderScheduler).
|
|
171
|
+
// The schedule runs the launcher (macOS) or a stable node.exe (Windows).
|
|
172
|
+
export async function refreshReminderSchedule(targetDir, {
|
|
173
|
+
nodePath = process.execPath,
|
|
174
|
+
loadConfig = loadReminderConfig,
|
|
175
|
+
install = installReminderScheduler
|
|
176
|
+
} = {}) {
|
|
177
|
+
try {
|
|
178
|
+
const config = await loadConfig();
|
|
97
179
|
if (!config?.enabled) {
|
|
98
180
|
return null;
|
|
99
181
|
}
|
|
100
|
-
await
|
|
101
|
-
return { ok: true };
|
|
182
|
+
await install(config, { home: targetDir, nodePath });
|
|
183
|
+
return { ok: true, nodePath };
|
|
102
184
|
} catch (error) {
|
|
103
185
|
return { ok: false, error: error.message };
|
|
104
186
|
}
|
|
@@ -407,6 +489,8 @@ export async function findCommandOnPath(commandName, {
|
|
|
407
489
|
|
|
408
490
|
export function parseSetupUserArgs(args = []) {
|
|
409
491
|
let loginMode = 'prompt';
|
|
492
|
+
let yes = false;
|
|
493
|
+
let mcpInstall = true;
|
|
410
494
|
|
|
411
495
|
for (const arg of args) {
|
|
412
496
|
if (arg === '--login') {
|
|
@@ -417,10 +501,18 @@ export function parseSetupUserArgs(args = []) {
|
|
|
417
501
|
loginMode = 'skip';
|
|
418
502
|
continue;
|
|
419
503
|
}
|
|
504
|
+
if (arg === '--yes' || arg === '-y') {
|
|
505
|
+
yes = true;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (arg === '--no-mcp-install') {
|
|
509
|
+
mcpInstall = false;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
420
512
|
throw new Error(`Unknown setup option: ${arg}`);
|
|
421
513
|
}
|
|
422
514
|
|
|
423
|
-
return { loginMode };
|
|
515
|
+
return { loginMode, yes, mcpInstall };
|
|
424
516
|
}
|
|
425
517
|
|
|
426
518
|
async function maybeRunAuthLogin(targetDir, { loginMode }) {
|
|
@@ -491,13 +583,14 @@ function runAuthLogin(targetDir) {
|
|
|
491
583
|
});
|
|
492
584
|
}
|
|
493
585
|
|
|
494
|
-
function printMcpInstructions(targetDir, authResult = null, linkResult = null, cleanupResult = null, reminderResult = null) {
|
|
586
|
+
function printMcpInstructions(targetDir, authResult = null, linkResult = null, cleanupResult = null, reminderResult = null, mcpResult = null) {
|
|
495
587
|
console.log(formatSetupUserInstructions({
|
|
496
588
|
targetDir,
|
|
497
589
|
authResult,
|
|
498
590
|
linkResult,
|
|
499
591
|
cleanupResult,
|
|
500
|
-
reminderResult
|
|
592
|
+
reminderResult,
|
|
593
|
+
mcpResult
|
|
501
594
|
}));
|
|
502
595
|
}
|
|
503
596
|
|
|
@@ -507,10 +600,14 @@ export function formatSetupUserInstructions({
|
|
|
507
600
|
linkResult = null,
|
|
508
601
|
cleanupResult = null,
|
|
509
602
|
reminderResult = null,
|
|
510
|
-
|
|
603
|
+
mcpResult = null,
|
|
604
|
+
platform = process.platform,
|
|
605
|
+
nodePath = process.execPath,
|
|
606
|
+
env = process.env,
|
|
607
|
+
launch = mcpResult?.launch || resolveLaunchCommand({ platform, home: targetDir, execPath: nodePath, env })
|
|
511
608
|
} = {}) {
|
|
512
|
-
const entryPath = resolve(targetDir, 'bin', 'logwork.mjs');
|
|
513
609
|
const storeLabel = credentialStoreLabel({ platform });
|
|
610
|
+
const mcp = formatMcpSection({ mcpResult, launch, platform });
|
|
514
611
|
const sections = [
|
|
515
612
|
`logwork-cli installed to ${targetDir}.`,
|
|
516
613
|
'',
|
|
@@ -526,27 +623,18 @@ export function formatSetupUserInstructions({
|
|
|
526
623
|
formatTerminalCommandInstructions(linkResult),
|
|
527
624
|
'',
|
|
528
625
|
'Next steps',
|
|
529
|
-
...formatNextSteps(authResult),
|
|
626
|
+
...formatNextSteps(authResult, mcp.manual),
|
|
530
627
|
'',
|
|
531
|
-
|
|
532
|
-
'',
|
|
533
|
-
'Cursor / Antigravity / Claude Code (.mcp.json):',
|
|
534
|
-
JSON.stringify(stdioMcpServersConfig(entryPath), null, 2),
|
|
535
|
-
'',
|
|
536
|
-
'Codex config.toml:',
|
|
537
|
-
formatCodexToml(entryPath),
|
|
538
|
-
'',
|
|
539
|
-
'GitHub Copilot / VS Code mcp.json:',
|
|
540
|
-
JSON.stringify(vscodeMcpConfig(entryPath), null, 2),
|
|
628
|
+
...mcp.lines,
|
|
541
629
|
'',
|
|
542
630
|
'AI agent skill (log work from your commits)',
|
|
543
631
|
'- Claude Code / Codex: run `logwork skill install`.',
|
|
544
632
|
'- Other MCP clients: use the MCP prompt "log_from_commits".',
|
|
545
633
|
'',
|
|
546
634
|
'Verify',
|
|
547
|
-
'- Restart or reload your IDE MCP tools after
|
|
635
|
+
'- Restart or reload your IDE MCP tools after a config change.',
|
|
548
636
|
'- In the IDE, ask: Check my logwork for this week.',
|
|
549
|
-
'-
|
|
637
|
+
'- `logwork mcp status` shows which clients have a current "logwork" entry.',
|
|
550
638
|
'',
|
|
551
639
|
'Safety',
|
|
552
640
|
'- Do not paste passwords, 2FA codes, Bearer tokens, cookies, or raw curl auth logs into AI chat.',
|
|
@@ -557,6 +645,70 @@ export function formatSetupUserInstructions({
|
|
|
557
645
|
return sections.join('\n');
|
|
558
646
|
}
|
|
559
647
|
|
|
648
|
+
// The MCP part of the setup summary. `manual` is true when the user still has
|
|
649
|
+
// to paste a config somewhere.
|
|
650
|
+
function formatMcpSection({ mcpResult, launch, platform }) {
|
|
651
|
+
const header = `MCP server "${MCP_SERVER_NAME}" runs: ${[launch.command, ...mcpArgs(launch)].join(' ')}`;
|
|
652
|
+
if (mcpResult?.mode === 'installed' && mcpResult.results?.length) {
|
|
653
|
+
const failed = mcpResult.results.some((result) => result.status === 'failed');
|
|
654
|
+
return {
|
|
655
|
+
manual: failed,
|
|
656
|
+
lines: [header, 'MCP clients', ...formatMcpInstallResults(mcpResult.results)]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (mcpResult?.mode === 'planned') {
|
|
660
|
+
const pending = (mcpResult.plans || []).some((plan) => plan.action === 'add' || plan.action === 'update');
|
|
661
|
+
if (mcpResult.plans?.length) {
|
|
662
|
+
return {
|
|
663
|
+
manual: false,
|
|
664
|
+
lines: [
|
|
665
|
+
header,
|
|
666
|
+
'MCP clients (not changed: this is not an interactive terminal)',
|
|
667
|
+
...(mcpResult.lines || []),
|
|
668
|
+
...(pending ? ['To apply: run `logwork mcp install` (asks per client) or `logwork mcp install --yes`.'] : [])
|
|
669
|
+
]
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (mcpResult?.mode === 'skipped') {
|
|
674
|
+
return {
|
|
675
|
+
manual: false,
|
|
676
|
+
lines: [header, 'MCP clients: not changed (--no-mcp-install). Run `logwork mcp install` to add or update them, or `logwork mcp status` to check.']
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
// No client found, an error, or an older caller: print every snippet.
|
|
680
|
+
const lines = [header];
|
|
681
|
+
if (mcpResult?.mode === 'error') {
|
|
682
|
+
lines.push(`MCP clients: could not update automatically (${redactText(mcpResult.error || 'unknown error')}).`);
|
|
683
|
+
} else if (mcpResult) {
|
|
684
|
+
lines.push('MCP clients: none found (Claude Code, Cursor, Codex, VS Code, Google Antigravity).');
|
|
685
|
+
}
|
|
686
|
+
lines.push(...formatManualMcpSnippets(launch, { platform }));
|
|
687
|
+
return { manual: true, lines };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function formatManualMcpSnippets(launch, { platform = process.platform } = {}) {
|
|
691
|
+
const note = launch.stable ? (launch.kind === 'launcher' ? MCP_LAUNCHER_NOTE : '') : MCP_NODE_PATH_NOTE;
|
|
692
|
+
return [
|
|
693
|
+
'Manual MCP config (paste into your client):',
|
|
694
|
+
...(note ? [note] : []),
|
|
695
|
+
'',
|
|
696
|
+
'Cursor / Antigravity / Claude Code (.mcp.json):',
|
|
697
|
+
JSON.stringify(stdioMcpServersConfig(launch), null, 2),
|
|
698
|
+
'',
|
|
699
|
+
'Claude Code CLI:',
|
|
700
|
+
formatClaudeMcpAddCommand(launch, { platform }),
|
|
701
|
+
'',
|
|
702
|
+
'Codex config.toml:',
|
|
703
|
+
formatCodexToml(launch),
|
|
704
|
+
'',
|
|
705
|
+
'GitHub Copilot / VS Code mcp.json:',
|
|
706
|
+
JSON.stringify(vscodeMcpConfig(launch), null, 2),
|
|
707
|
+
'',
|
|
708
|
+
'An old "logwork-helper" MCP entry can be deleted; it points at the old runtime.'
|
|
709
|
+
];
|
|
710
|
+
}
|
|
711
|
+
|
|
560
712
|
function formatAuthStatus(authResult) {
|
|
561
713
|
if (authResult?.ok) {
|
|
562
714
|
return 'completed';
|
|
@@ -567,7 +719,7 @@ function formatAuthStatus(authResult) {
|
|
|
567
719
|
return 'not run';
|
|
568
720
|
}
|
|
569
721
|
|
|
570
|
-
function formatNextSteps(authResult) {
|
|
722
|
+
function formatNextSteps(authResult, manualMcp = true) {
|
|
571
723
|
const steps = [];
|
|
572
724
|
let index = 1;
|
|
573
725
|
if (!authResult?.ok) {
|
|
@@ -575,9 +727,12 @@ function formatNextSteps(authResult) {
|
|
|
575
727
|
steps.push(' logwork login');
|
|
576
728
|
index += 1;
|
|
577
729
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
730
|
+
if (manualMcp) {
|
|
731
|
+
steps.push(`${index}. Paste the MCP config below into your IDE (or run \`logwork mcp install\`).`);
|
|
732
|
+
index += 1;
|
|
733
|
+
}
|
|
734
|
+
steps.push(`${index}. Restart or reload your IDE MCP tools.`);
|
|
735
|
+
steps.push(`${index + 1}. Verify with: Check my logwork for this week.`);
|
|
581
736
|
return steps;
|
|
582
737
|
}
|
|
583
738
|
|
package/commands/tui.mjs
CHANGED
|
@@ -43,6 +43,7 @@ Common commands inside the UI:
|
|
|
43
43
|
/reconcile this-week Compare RO and Jira
|
|
44
44
|
/projects List RO projects
|
|
45
45
|
/map SCB 5234 Map a ticket prefix to a project
|
|
46
|
+
/map --repo . 5234 Map this git repo to a project (commit drafts)
|
|
46
47
|
/mcp Show MCP config for an IDE
|
|
47
48
|
/history Local apply history
|
|
48
49
|
|
package/docs/advanced.md
CHANGED
|
@@ -27,7 +27,7 @@ Supported overrides:
|
|
|
27
27
|
```text
|
|
28
28
|
LOGWORK_PROFILE (LOGWORK_HELPER_PROFILE still accepted)
|
|
29
29
|
LOGWORK_HOME (local state directory; LOGWORK_HELPER_HOME still accepted)
|
|
30
|
-
LOGWORK_DRY_RUN=1 (RO writes return the request
|
|
30
|
+
LOGWORK_DRY_RUN=1 (RO and Jira writes return the request instead of calling the API)
|
|
31
31
|
LOGWORK_API_BASE
|
|
32
32
|
LOGWORK_LOGIN_URL
|
|
33
33
|
LOGWORK_JIRA_BASE_URL
|
|
@@ -78,9 +78,9 @@ To install and start auth immediately:
|
|
|
78
78
|
logwork setup --login
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Use `--no-login` when installing in scripts or CI.
|
|
81
|
+
Use `--no-login` when installing in scripts or CI. Add `--no-mcp-install` to leave MCP client configs alone, or `--yes` to update every detected client without asking. Without a terminal and without `--yes`, setup only prints what `logwork mcp install` would change.
|
|
82
82
|
|
|
83
|
-
`logwork setup` copies the package into `~/.logwork-cli`, runs `npm ci --omit=dev` there, removes a globally installed `logwork-helper` package (its `logwork` bin would block linking), runs `npm link`, re-registers an enabled reminder, and
|
|
83
|
+
`logwork setup` copies the package into `~/.logwork-cli`, runs `npm ci --omit=dev` there, removes a globally installed `logwork-helper` package (its `logwork` bin would block linking), runs `npm link`, writes `runtime.json` and the launcher (`bin/logwork-launch`, macOS), re-registers an enabled reminder, and adds or updates the `logwork` entry in the detected MCP clients ([details](mcp-setup.md#automatic-setup)). It prints the manual snippets only for clients it could not update, or when it found none.
|
|
84
84
|
|
|
85
85
|
## Update
|
|
86
86
|
|
|
@@ -91,7 +91,7 @@ logwork update install # interactive confirmation
|
|
|
91
91
|
logwork update install --version 0.3.1 --yes
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
The installer accepts only the version currently published under npm's `latest` tag. It runs `npm exec --package=logwork-cli@<version> -- logwork setup --no-login` with a fixed argument list. Then it verifies the copied `package.json` version in `~/.logwork-cli`.
|
|
94
|
+
The installer accepts only the version currently published under npm's `latest` tag. It runs `npm exec --package=logwork-cli@<version> -- logwork setup --no-login` with a fixed argument list. Then it verifies the copied `package.json` version in `~/.logwork-cli`. That setup runs without a terminal, so it does not edit MCP configs; they keep working because they point at the launcher (macOS) or a stable `node.exe` (Windows). `logwork mcp status` shows whether anything needs `logwork mcp install`.
|
|
95
95
|
|
|
96
96
|
Project mappings, drafts, apply history, update cache, and OS credential-store records are preserved.
|
|
97
97
|
|
|
@@ -222,6 +222,8 @@ Useful commands inside the session:
|
|
|
222
222
|
/projects
|
|
223
223
|
/projects 5234
|
|
224
224
|
/map SCB 5234
|
|
225
|
+
/map --repo
|
|
226
|
+
/map --repo ~/code/fcs GKE Fair Chem
|
|
225
227
|
/history
|
|
226
228
|
/diagnostics
|
|
227
229
|
```
|
|
@@ -271,8 +273,8 @@ Inside `logwork`, use `/history`, `/history ro`, `/history jira`, or `/history b
|
|
|
271
273
|
|
|
272
274
|
## Troubleshooting
|
|
273
275
|
|
|
274
|
-
- **IDE does not show tools:** restart or reload the MCP client.
|
|
275
|
-
- **Tools show old names such as `preview_logwork_batch`:** the IDE is still connected to the old `logwork-helper` entry.
|
|
276
|
+
- **IDE does not show tools:** run `logwork mcp status`. If the client's entry is missing or stale, run `logwork mcp install`, then restart or reload the MCP client. The entry should run `~/.logwork-cli/bin/logwork-launch mcp` (on Windows, `node.exe %USERPROFILE%\.logwork-cli\bin\logwork.mjs mcp`) under the server name `logwork`. `logwork doctor` also checks that the launcher finds a usable Node.
|
|
277
|
+
- **Tools show old names such as `preview_logwork_batch`:** the IDE is still connected to the old `logwork-helper` entry. `logwork mcp install` removes it; the [tool name table](mcp-setup.md#tool-name-changes-from-logwork-helper) lists the renames.
|
|
276
278
|
- **`logwork: command not found` after `logwork setup`:** open a new terminal first. If it still fails, run `npm install -g logwork-cli`, or make sure your npm global bin directory is on `PATH`.
|
|
277
279
|
- **`npm link` fails with `EEXIST` on `logwork`:** an old global `logwork-helper` package still owns the bin. Run `npm rm -g logwork-helper`, then `logwork setup` again.
|
|
278
280
|
- **Not authenticated to Resource Optimiser:** run `logwork login`, or ask the assistant to call `login`. Enter secrets only in the terminal.
|
|
@@ -285,11 +287,20 @@ Inside `logwork`, use `/history`, `/history ro`, `/history jira`, or `/history b
|
|
|
285
287
|
|
|
286
288
|
## Dry Run
|
|
287
289
|
|
|
288
|
-
`LOGWORK_DRY_RUN=1` makes Resource Optimiser
|
|
290
|
+
`LOGWORK_DRY_RUN=1` makes every Resource Optimiser and Jira write return the request (`{ dryRun: true, method, path, body }`) instead of calling the API. The request never contains the token or the `Authorization` header. Reads still hit the API: Jira issue lookup and the duplicate-worklog check still call Jira, and RO previews still read RO.
|
|
291
|
+
|
|
292
|
+
What a dry run reports:
|
|
293
|
+
|
|
294
|
+
- RO apply summaries start with `DRY RUN`; edit, resubmit and delete return `status: "dry_run"`.
|
|
295
|
+
- Jira apply (`log_jira_work` with `confirm: true`, or the terminal UI `/apply`) returns `status: "dry_run"`, `dryRun: true`, one `dry_run` result per entry with its `request`, and a summary that starts with `DRY RUN (LOGWORK_DRY_RUN=1)`.
|
|
296
|
+
- Apply history records a Jira dry run with status `dry_run` and `dryRun: true` entries. They are not counted as submitted.
|
|
297
|
+
- The terminal UI status bar shows `DRY RUN`.
|
|
289
298
|
|
|
290
299
|
```bash
|
|
291
300
|
LOGWORK_DRY_RUN=1 logwork quick -m "Dry run task"
|
|
292
301
|
LOGWORK_DRY_RUN=1 logwork edit 304032 --desc "Dry run" --yes
|
|
302
|
+
npm run dry-run # terminal UI, RO and Jira writes stubbed
|
|
303
|
+
LOGWORK_DRY_RUN=1 logwork mcp # MCP server, RO and Jira writes stubbed
|
|
293
304
|
```
|
|
294
305
|
|
|
295
306
|
In Windows PowerShell:
|