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/README.md
CHANGED
|
@@ -26,7 +26,7 @@ logwork setup
|
|
|
26
26
|
- installs the runtime to `~/.logwork-cli` (`%USERPROFILE%\.logwork-cli` on Windows),
|
|
27
27
|
- links the `logwork` command,
|
|
28
28
|
- offers to log in,
|
|
29
|
-
-
|
|
29
|
+
- adds the `logwork` MCP server to the AI clients it finds (Claude Code, Cursor, Codex, VS Code, Google Antigravity). It asks once per client; answer yes. See [Set Up MCP](#set-up-mcp).
|
|
30
30
|
|
|
31
31
|
## Upgrading from logwork-helper
|
|
32
32
|
|
|
@@ -57,7 +57,7 @@ Most of the move is automatic:
|
|
|
57
57
|
|
|
58
58
|
Two things you need to do yourself:
|
|
59
59
|
|
|
60
|
-
- **IDE config:**
|
|
60
|
+
- **IDE config:** `logwork setup` replaces the old `logwork-helper` entry with the new `logwork` entry in the clients it can edit. For any other client, use the config printed by `logwork setup` or `/mcp`.
|
|
61
61
|
- **MCP tool names:** the tool names changed. See the [old → new tool name table](docs/mcp-setup.md#tool-name-changes-from-logwork-helper).
|
|
62
62
|
|
|
63
63
|
## Log In
|
|
@@ -77,7 +77,7 @@ Sessions are stored in macOS Keychain or Windows Credential Manager. The default
|
|
|
77
77
|
```text
|
|
78
78
|
logwork Open the terminal UI
|
|
79
79
|
logwork query [period] Booked vs logged hours with entry ids (today, yesterday, this-week, last-week, this-month, last-month)
|
|
80
|
-
logwork commits [period] Draft logwork lines from your git commits (--repo, --author)
|
|
80
|
+
logwork commits [period] Draft logwork lines from your git commits (--repo, --author, --no-prompt)
|
|
81
81
|
logwork quick [-m text] Log one entry for today with guided prompts
|
|
82
82
|
logwork edit <id> ... Change an existing RO entry (replaces it)
|
|
83
83
|
logwork delete <id> Delete a submitted or approved RO entry
|
|
@@ -85,7 +85,9 @@ logwork reconcile Compare RO and Jira hours by day (read-only)
|
|
|
85
85
|
logwork history Local apply history
|
|
86
86
|
logwork login|logout|status [ro|jira]
|
|
87
87
|
logwork skill install Install the AI agent skill (Claude Code, Codex)
|
|
88
|
-
logwork setup Install the runtime and
|
|
88
|
+
logwork setup Install the runtime and add logwork to your MCP clients
|
|
89
|
+
logwork mcp install Add or update the logwork entry in your MCP clients
|
|
90
|
+
logwork mcp status Check each MCP client's logwork entry
|
|
89
91
|
logwork mcp Run the MCP stdio server (used by IDE configs)
|
|
90
92
|
logwork reminder ... Weekday logwork reminder
|
|
91
93
|
logwork update ... Check for or install a new version
|
|
@@ -120,12 +122,20 @@ How the draft is built:
|
|
|
120
122
|
| `chore` | Other Activities |
|
|
121
123
|
|
|
122
124
|
Conventional prefixes are used first, then keywords in the subject.
|
|
123
|
-
- **
|
|
125
|
+
- **Project:** a project mapping whose `repos` list holds the repo wins. Otherwise ticket and keyword mappings and the day's booked projects pick it, the same way `log_work` does.
|
|
126
|
+
- **Repos with no project:** you are asked once, in place. In an interactive terminal, `logwork commits` shows `Which RO project does <repo> belong to?` with your member projects. The likely project is preselected, with the reason as a hint:
|
|
127
|
+
- a ticket key from the repo's commits that you logged to that project in the last 30 days;
|
|
128
|
+
- the repo folder or git remote name matching the project name, its acronym in parentheses (`fcs-web` → `GKE Fair Chem System (FCS)`) or its initials;
|
|
129
|
+
- the only project booked on the repo's commit days.
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
Your choice is saved as a repo mapping (user scope) and the draft is redone. Choose `Skip for now` to be asked again next time. `--no-prompt`, `--json` or a non-interactive terminal never ask; the summary then names the likely project per repo.
|
|
132
|
+
- **Hours:** each project's RO remaining time (booked − logged) is split across that project's groups in 0.5h steps, weighted by commits and lines changed, and never exceeds it. A project with no room left gets 0h. Groups without a project share the room of booked projects that no group claimed. Days without a booking get 0h. Without RO access, a weekday counts as 8h, split across all groups.
|
|
133
|
+
|
|
134
|
+
Nothing is written to RO or Jira. Paste the draft into `/logwork`, or let your AI agent use it.
|
|
126
135
|
|
|
127
136
|
For agents:
|
|
128
|
-
- **MCP tool `draft_from_commits`** returns the same draft as `draftText
|
|
137
|
+
- **MCP tool `draft_from_commits`** returns the same draft as `draftText`, plus `projectOverrides` (preview entry id → `projectMemberId`) for every line with a project. The agent passes both to `log_work` unchanged. In the terminal UI, pasted lines are resolved again from mappings and bookings.
|
|
138
|
+
- For `unmappedRepos` it also returns `repoSuggestions` (ranked projects with reasons per repo) and `projectCandidates`. The agent asks you one short question per repo by project name ("Is fcs-web part of GKE Fair Chem System (FCS)?"), then saves the answer with `map_project` and drafts again. You never type an id.
|
|
129
139
|
- **MCP prompt `log_from_commits`** gives an agent the full procedure:
|
|
130
140
|
1. find gaps with `get_logwork`;
|
|
131
141
|
2. draft from commits;
|
|
@@ -184,28 +194,33 @@ The default tasks shared by all projects are:
|
|
|
184
194
|
|
|
185
195
|
## Set Up MCP
|
|
186
196
|
|
|
187
|
-
`logwork setup`
|
|
197
|
+
Run `logwork setup` and answer yes for each client. It finds the clients you have and adds or updates the `logwork` server in each:
|
|
188
198
|
|
|
189
|
-
|
|
199
|
+
| Client | What setup changes |
|
|
200
|
+
|---|---|
|
|
201
|
+
| Claude Code | `claude mcp add --scope user logwork ...` (or `~/.claude.json` when the `claude` CLI is not on `PATH`) |
|
|
202
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
203
|
+
| Codex | `~/.codex/config.toml` |
|
|
204
|
+
| VS Code | user `mcp.json` (`~/Library/Application Support/Code/User` on macOS, `%APPDATA%\Code\User` on Windows) |
|
|
205
|
+
| Google Antigravity | `~/.gemini/antigravity/mcp_config.json` |
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
{
|
|
193
|
-
"mcpServers": {
|
|
194
|
-
"logwork": {
|
|
195
|
-
"command": "node",
|
|
196
|
-
"args": ["/Users/you/.logwork-cli/bin/logwork.mjs", "mcp"]
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
For Claude Code, run:
|
|
207
|
+
It touches only the `logwork` entry, removes an old `logwork-helper` entry, and backs up each file first (`<file>.logwork-backup-<timestamp>`). A file it cannot parse safely (JSON with comments, unusual TOML) is left alone, and setup prints the snippet to paste instead.
|
|
203
208
|
|
|
204
209
|
```bash
|
|
205
|
-
|
|
210
|
+
logwork mcp status # which clients have a current logwork entry
|
|
211
|
+
logwork mcp install # add or update them later (asks per client)
|
|
212
|
+
logwork mcp install --dry-run # show the changes without writing
|
|
213
|
+
logwork setup --yes # update every detected client without asking
|
|
214
|
+
logwork setup --no-mcp-install
|
|
206
215
|
```
|
|
207
216
|
|
|
208
|
-
|
|
217
|
+
The configs never name a Node version, so Node upgrades need no config change:
|
|
218
|
+
- **macOS:** clients run `~/.logwork-cli/bin/logwork-launch mcp`. This small script, written by `logwork setup`, starts logwork with the first Node 22+ it finds: the Node that ran setup, `node` on `PATH`, the newest nvm Node, Volta or fnm, then Homebrew.
|
|
219
|
+
- **Windows:** clients run `node.exe %USERPROFILE%\.logwork-cli\bin\logwork.mjs mcp` with a stable `node.exe`: the nvm-windows symlink (`%NVM_SYMLINK%\node.exe`) or `C:\Program Files\nodejs\node.exe`.
|
|
220
|
+
|
|
221
|
+
For a client setup does not know, open the terminal UI with `logwork` and type `/mcp` (or `/mcp cursor`, `/mcp antigravity`, `/mcp copilot`, `/mcp claude-code`, `/mcp codex`) for a copy-ready config. [docs/mcp-setup.md](docs/mcp-setup.md) has the manual snippets.
|
|
222
|
+
|
|
223
|
+
Restart or reload the IDE's MCP tools after a change.
|
|
209
224
|
|
|
210
225
|
The server exposes 16 tools and one prompt (`log_from_commits`). It also sends `instructions` covering the approval rules, the line format and the recommended call order. Every write is **two calls to the same tool**:
|
|
211
226
|
1. Call the tool without `confirm`. It returns a preview with a `batchId` or `previewId` and a summary.
|
|
@@ -289,7 +304,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
|
|
|
289
304
|
- `/query this-week`
|
|
290
305
|
- `/reconcile`
|
|
291
306
|
- `/projects`
|
|
292
|
-
- `/map SCB 5234`
|
|
307
|
+
- `/map --repo` (this git repo, for commit drafts: opens a project picker with the likely project first), `/map --repo ~/code/fcs GKE Fair` (by name), or `/map SCB 5234` (ticket prefix)
|
|
293
308
|
- `/history`
|
|
294
309
|
- `/diagnostics`
|
|
295
310
|
|
|
@@ -298,6 +313,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
|
|
|
298
313
|
```bash
|
|
299
314
|
logwork reminder enable # 17:00 Monday-Friday, RO and Jira
|
|
300
315
|
logwork reminder enable --time 18:00 --target ro
|
|
316
|
+
logwork reminder enable --repo ~/code/app --repo ~/code/api # add commit evidence
|
|
301
317
|
logwork reminder test
|
|
302
318
|
logwork reminder status
|
|
303
319
|
logwork reminder disable
|
|
@@ -305,7 +321,19 @@ logwork reminder disable
|
|
|
305
321
|
|
|
306
322
|
For Resource Optimiser, the reminder compares today's logged and booked hours. For Jira, it checks whether you have any worklog today. Complete days stay silent.
|
|
307
323
|
|
|
308
|
-
|
|
324
|
+
With `--repo` (repeatable; `--no-repos` clears the list), a reminder about missing RO hours also counts your commits today in those repos and points you to a draft:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
Logwork hôm nay chưa hoàn tất
|
|
328
|
+
RO: 4/8h, còn thiếu 4h
|
|
329
|
+
Hôm nay: 5 commit trong 2 repo
|
|
330
|
+
|
|
331
|
+
Mở Terminal và chạy: logwork commits today (bản nháp từ commit)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Repos listed on project mappings are used too. Run outside a git repo, `logwork commits today` falls back to these repos. If reading commits fails, the reminder shows the plain message.
|
|
335
|
+
|
|
336
|
+
macOS uses a user `launchd` agent (`sg.vinova.logwork-cli.reminder`) and Windows uses Task Scheduler (`Logwork Reminder`). Neither schedule stores credentials. On macOS the schedule runs the `logwork-launch` launcher, so Node upgrades do not break it. On Windows it runs a stable `node.exe`. A schedule from an older version that still runs a raw Node path is flagged by `logwork reminder status`; run `logwork setup` to re-register it.
|
|
309
337
|
|
|
310
338
|
## Reconcile RO And Jira
|
|
311
339
|
|
package/RELEASE.md
CHANGED
|
@@ -55,6 +55,7 @@ This checks version consistency, git cleanliness, npm auth, npm latest version,
|
|
|
55
55
|
|
|
56
56
|
The gate must pass all of the following:
|
|
57
57
|
|
|
58
|
+
- `npm run lint`
|
|
58
59
|
- `npm test`
|
|
59
60
|
- `npm audit --omit=dev --audit-level=moderate`
|
|
60
61
|
- `npm pack --dry-run`
|
|
@@ -63,8 +64,9 @@ The gate must pass all of the following:
|
|
|
63
64
|
## Manual Verification
|
|
64
65
|
|
|
65
66
|
- Install from the packed tarball in a temporary directory.
|
|
66
|
-
- Run `logwork setup --no-login
|
|
67
|
-
- Confirm
|
|
67
|
+
- Run `logwork setup --no-login` with a temporary `HOME` and `LOGWORK_HOME`, so no real MCP client config is touched.
|
|
68
|
+
- Confirm `~/.logwork-cli/bin/logwork-launch` exists and is executable (macOS), and that `logwork-launch --version` prints the package version.
|
|
69
|
+
- Confirm `logwork mcp install --dry-run` lists the detected clients with server name `logwork` and command `~/.logwork-cli/bin/logwork-launch mcp` (Windows: `node.exe ...\bin\logwork.mjs mcp`), and that `logwork mcp status` reports them.
|
|
68
70
|
- Run `logwork --help` and `logwork --version`.
|
|
69
71
|
- Run `logwork update check --force` against the published npm version.
|
|
70
72
|
- Run `logwork mcp` through an MCP client smoke test and confirm it lists the 16 tools (`get_logwork`, `draft_from_commits`, `list_projects`, `list_tasks`, `log_work`, `edit_entry`, `resubmit_entry`, `delete_entry`, `log_jira_work`, `get_jira_issue`, `reconcile`, `get_history`, `map_project`, `login`, `reminder`, `update`).
|
package/bin/logwork.mjs
CHANGED
|
@@ -20,8 +20,8 @@ export const COMMANDS = [
|
|
|
20
20
|
{ name: 'logout', usage: 'logout [ro|jira]', summary: 'Remove the stored RO (default) or Jira session', load: () => import('../commands/auth.mjs'), fn: 'runLogout' },
|
|
21
21
|
{ name: 'status', usage: 'status', summary: 'Show RO and Jira login status', load: () => import('../commands/auth.mjs'), fn: 'runStatus' },
|
|
22
22
|
{ name: 'skill', usage: 'skill [print|install] [--target claude|codex|all]', summary: 'Print or install the AI agent skill for logging from commits', load: () => import('../commands/skill.mjs'), fn: 'run' },
|
|
23
|
-
{ name: 'setup', usage: 'setup [--login|--no-login]', summary: 'Install the runtime and
|
|
24
|
-
{ name: 'mcp', usage: 'mcp', summary: 'Run the MCP stdio server
|
|
23
|
+
{ name: 'setup', usage: 'setup [--yes] [--no-mcp-install] [--login|--no-login]', summary: 'Install the runtime and add logwork to your MCP clients', load: () => import('../commands/setup.mjs'), fn: 'run' },
|
|
24
|
+
{ name: 'mcp', usage: 'mcp [install|status]', summary: 'Run the MCP stdio server; install or check IDE configs', load: () => import('../commands/mcp.mjs'), fn: 'run' },
|
|
25
25
|
{ name: 'reminder', usage: 'reminder enable|disable|status|test', summary: 'Manage the weekday logwork reminder', load: () => import('../commands/reminder.mjs'), fn: 'runReminderCli' },
|
|
26
26
|
{ name: 'update', usage: 'update [check|install]', summary: 'Check for or install a new version', load: () => import('../commands/update.mjs'), fn: 'runUpdateCli' },
|
|
27
27
|
{ name: 'doctor', usage: 'doctor', summary: 'Write a sanitized diagnostics report', load: () => import('../commands/doctor.mjs'), fn: 'run' },
|
package/commands/commits.mjs
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
1
3
|
import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
|
|
4
|
+
import { upsertProjectMapping } from '../lib/project-mapping-workflow.mjs';
|
|
2
5
|
import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
|
|
6
|
+
import { loadReminderConfig } from '../lib/reminder-config.mjs';
|
|
7
|
+
import { collectReminderRepos } from '../lib/reminder-workflow.mjs';
|
|
8
|
+
import { chooseRepoProject } from '../lib/ui.mjs';
|
|
3
9
|
|
|
4
|
-
// `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
|
|
5
|
-
// turn your git commits into a logwork draft.
|
|
10
|
+
// `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--no-prompt] [--json]`:
|
|
11
|
+
// turn your git commits into a logwork draft. Never writes to RO or Jira; in an
|
|
12
|
+
// interactive terminal it asks once per unmapped repo which project it belongs
|
|
13
|
+
// to and saves the answer as a local repo mapping.
|
|
6
14
|
export function parseCommitsArgs(args = []) {
|
|
7
|
-
const options = { period: 'today', repos: [], author: undefined, useBookings: true, json: false, help: false };
|
|
15
|
+
const options = { period: 'today', repos: [], author: undefined, useBookings: true, json: false, prompt: true, help: false };
|
|
8
16
|
const positional = [];
|
|
9
17
|
for (let index = 0; index < args.length; index += 1) {
|
|
10
18
|
const arg = args[index];
|
|
@@ -14,6 +22,8 @@ export function parseCommitsArgs(args = []) {
|
|
|
14
22
|
options.json = true;
|
|
15
23
|
} else if (arg === '--no-bookings') {
|
|
16
24
|
options.useBookings = false;
|
|
25
|
+
} else if (arg === '--no-prompt') {
|
|
26
|
+
options.prompt = false;
|
|
17
27
|
} else if (arg === '--repo' || arg === '--author') {
|
|
18
28
|
const value = args[index + 1];
|
|
19
29
|
if (!value || value.startsWith('--')) {
|
|
@@ -44,29 +54,125 @@ export function parseCommitsArgs(args = []) {
|
|
|
44
54
|
return options;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
export async function run(args = [], {
|
|
57
|
+
export async function run(args = [], {
|
|
58
|
+
draft = draftLogworkFromCommits,
|
|
59
|
+
print = console.log,
|
|
60
|
+
cwd = process.cwd(),
|
|
61
|
+
reminderRepos = loadReminderRepos,
|
|
62
|
+
isInteractive = defaultIsInteractive,
|
|
63
|
+
chooseProject = chooseRepoProject,
|
|
64
|
+
saveMapping = upsertProjectMapping
|
|
65
|
+
} = {}) {
|
|
48
66
|
const options = parseCommitsArgs(args);
|
|
49
67
|
if (options.help) {
|
|
50
68
|
print(formatCommitsHelp());
|
|
51
69
|
return 0;
|
|
52
70
|
}
|
|
53
|
-
|
|
71
|
+
// The reminder tells the user to run `logwork commits today` from a fresh
|
|
72
|
+
// terminal, which starts outside any repo. There, fall back to the
|
|
73
|
+
// reminder's repos instead of reporting no commits.
|
|
74
|
+
let repos = options.repos;
|
|
75
|
+
let fallbackNote = '';
|
|
76
|
+
if (!repos.length && !isInsideGitWorkTree(cwd)) {
|
|
77
|
+
const fallback = await reminderRepos().catch(() => []);
|
|
78
|
+
if (fallback.length) {
|
|
79
|
+
repos = fallback;
|
|
80
|
+
fallbackNote = `Not in a git repo; using the reminder repos: ${fallback.join(', ')}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const draftOptions = {
|
|
54
84
|
period: options.period,
|
|
55
|
-
repos
|
|
85
|
+
repos,
|
|
56
86
|
author: options.author,
|
|
57
87
|
useBookings: options.useBookings
|
|
58
|
-
}
|
|
59
|
-
|
|
88
|
+
};
|
|
89
|
+
const result = await draft(draftOptions);
|
|
90
|
+
if (options.json) {
|
|
91
|
+
print(JSON.stringify(result, null, 2));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
print(fallbackNote ? `${fallbackNote}\n\n${result.summary}` : result.summary);
|
|
95
|
+
|
|
96
|
+
if (!options.prompt || !isInteractive() || !result.unmappedRepos?.length) {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
const mapped = await askRepoProjects(result, { chooseProject, saveMapping, print, cwd });
|
|
100
|
+
if (mapped) {
|
|
101
|
+
const updated = await draft(draftOptions);
|
|
102
|
+
print(`\nUpdated draft:\n\n${updated.summary}`);
|
|
103
|
+
}
|
|
60
104
|
return 0;
|
|
61
105
|
}
|
|
62
106
|
|
|
107
|
+
// One select prompt per unmapped repo (once per run); a chosen project is
|
|
108
|
+
// saved as a user-scope repo mapping. Returns how many repos were mapped.
|
|
109
|
+
export async function askRepoProjects(result, { chooseProject = chooseRepoProject, saveMapping = upsertProjectMapping, print = console.log, cwd = process.cwd() } = {}) {
|
|
110
|
+
const candidates = result.projectCandidates || [];
|
|
111
|
+
if (!candidates.length) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
let mapped = 0;
|
|
115
|
+
for (const repo of result.unmappedRepos) {
|
|
116
|
+
const repoName = basename(repo) || repo;
|
|
117
|
+
const project = await chooseProject({
|
|
118
|
+
repoName,
|
|
119
|
+
suggestions: result.repoSuggestions?.[repo] || [],
|
|
120
|
+
candidates
|
|
121
|
+
});
|
|
122
|
+
if (!project) {
|
|
123
|
+
print(`Skipped ${repoName}; it will be asked again next time.`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await saveMapping({
|
|
128
|
+
repos: [repo],
|
|
129
|
+
...(project.projectMemberId !== undefined ? { projectMemberId: project.projectMemberId } : { projectName: project.projectName }),
|
|
130
|
+
scope: 'user',
|
|
131
|
+
confirm: true,
|
|
132
|
+
cwd
|
|
133
|
+
});
|
|
134
|
+
print(`Mapped ${repoName} to ${project.projectName}. Later drafts use it automatically.`);
|
|
135
|
+
mapped += 1;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
print(`Could not map ${repoName}: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return mapped;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function defaultIsInteractive() {
|
|
144
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// True when dir or a parent holds .git (a directory, or a file for worktrees
|
|
148
|
+
// and submodules). No git process is started.
|
|
149
|
+
export function isInsideGitWorkTree(dir, { exists = existsSync } = {}) {
|
|
150
|
+
let current = resolve(dir);
|
|
151
|
+
for (;;) {
|
|
152
|
+
if (exists(join(current, '.git'))) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const parent = dirname(current);
|
|
156
|
+
if (parent === current) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
current = parent;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function loadReminderRepos() {
|
|
164
|
+
return collectReminderRepos({ config: await loadReminderConfig() });
|
|
165
|
+
}
|
|
166
|
+
|
|
63
167
|
export function formatCommitsHelp() {
|
|
64
168
|
return `Usage:
|
|
65
169
|
logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
|
|
66
|
-
[--repo <path>]... [--author <email>] [--no-bookings]
|
|
170
|
+
[--repo <path>]... [--author <email>] [--no-bookings]
|
|
171
|
+
[--no-prompt] [--json]
|
|
67
172
|
|
|
68
173
|
Reads your commits (all branches, your git user.email) from each repo (default:
|
|
69
|
-
the current directory
|
|
174
|
+
the current directory; outside a git repo, the repos set with
|
|
175
|
+
\`logwork reminder enable --repo\`) and drafts logwork lines:
|
|
70
176
|
+<hours> <worklog task> | <description>
|
|
71
177
|
|
|
72
178
|
- Commits are grouped per day by repo, ticket key and kind of work.
|
|
@@ -74,9 +180,18 @@ the current directory) and drafts logwork lines:
|
|
|
74
180
|
fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
|
|
75
181
|
test -> Testing Activities, ci/build/release -> Deployment & Release,
|
|
76
182
|
docs/design -> Requirements & Design, chore -> Other Activities.
|
|
77
|
-
-
|
|
78
|
-
|
|
183
|
+
- Each group gets an RO project: a mapping whose repos holds the repo, else
|
|
184
|
+
ticket/keyword mappings and that day's bookings.
|
|
185
|
+
- For a repo with no project, an interactive terminal asks once: "Which RO
|
|
186
|
+
project does <repo> belong to?", with the likely project preselected (ticket
|
|
187
|
+
history, repo/remote name, single booked project). The answer is saved as a
|
|
188
|
+
repo mapping and the draft is redone. Skip it to be asked again next time;
|
|
189
|
+
--no-prompt (or --json, or a non-interactive terminal) never asks.
|
|
190
|
+
- Each project's RO remaining time (booked - logged) is split across its
|
|
191
|
+
groups by how much work each holds; groups without a project share the room
|
|
192
|
+
of booked projects no group claimed. With --no-bookings or without login, a
|
|
193
|
+
weekday is 8h split across all groups.
|
|
79
194
|
|
|
80
|
-
Nothing is written. Review the draft, then paste it into /logwork or give it
|
|
195
|
+
Nothing is written to RO or Jira. Review the draft, then paste it into /logwork or give it
|
|
81
196
|
to your AI agent (log_work).`;
|
|
82
197
|
}
|
package/commands/history.mjs
CHANGED
package/commands/mcp.mjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// `logwork mcp`: with no arguments, the MCP stdio server that every IDE config
|
|
2
|
+
// runs. Nothing may be written to stdout before the server starts, because
|
|
3
|
+
// stdout is the protocol channel. `install`, `status` and `--help` are
|
|
4
|
+
// dispatched first. The modules imported here never write to stdout.
|
|
5
|
+
import { constants as fsConstants, promises as fs } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { writeLauncher } from '../lib/launcher.mjs';
|
|
8
|
+
import { helperHome } from '../lib/paths.mjs';
|
|
9
|
+
import {
|
|
10
|
+
MCP_INSTALL_CLIENT_KEYS,
|
|
11
|
+
applyMcpClientPlan,
|
|
12
|
+
createMcpInstallContext,
|
|
13
|
+
describeMcpPlan,
|
|
14
|
+
inspectMcpClients,
|
|
15
|
+
normalizeMcpInstallClient,
|
|
16
|
+
planMcpInstall
|
|
17
|
+
} from '../lib/mcp-install.mjs';
|
|
18
|
+
|
|
19
|
+
export async function run(argv = []) {
|
|
20
|
+
if (!argv.length) {
|
|
21
|
+
const { startMcpServer } = await import('../lib/mcp-server.mjs');
|
|
22
|
+
return startMcpServer();
|
|
23
|
+
}
|
|
24
|
+
const [sub, ...rest] = argv;
|
|
25
|
+
if (sub === '--help' || sub === '-h' || sub === 'help') {
|
|
26
|
+
console.log(formatMcpHelp());
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (sub === 'install') {
|
|
30
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
31
|
+
console.log(formatMcpHelp());
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
return runMcpInstallCli(parseMcpInstallArgs(rest));
|
|
35
|
+
}
|
|
36
|
+
if (sub === 'status') {
|
|
37
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
38
|
+
console.log(formatMcpHelp());
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (rest.length) {
|
|
42
|
+
throw new Error(`Unknown mcp status option: ${rest[0]}`);
|
|
43
|
+
}
|
|
44
|
+
return runMcpStatusCli();
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Unknown mcp command: ${sub}. Run \`logwork mcp --help\`.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatMcpHelp() {
|
|
50
|
+
return `Usage:
|
|
51
|
+
logwork mcp Run the MCP stdio server (what IDE configs run)
|
|
52
|
+
logwork mcp install [--client claude|cursor|codex|vscode|antigravity]... [--yes] [--dry-run]
|
|
53
|
+
logwork mcp status
|
|
54
|
+
|
|
55
|
+
install Adds or updates the "logwork" server in the MCP clients found on this
|
|
56
|
+
machine (Claude Code, Cursor, Codex, VS Code, Google Antigravity).
|
|
57
|
+
Asks once per client unless --yes. Each edited file is backed up next
|
|
58
|
+
to itself first (<file>.logwork-backup-<timestamp>). An old
|
|
59
|
+
"logwork-helper" entry is removed.
|
|
60
|
+
status Shows each client and whether its "logwork" entry is current, stale
|
|
61
|
+
or missing.
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
--client <name> Only this client (repeatable)
|
|
65
|
+
--yes, -y Update every selected client without asking
|
|
66
|
+
--dry-run Show the planned changes without writing anything`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseMcpInstallArgs(args = []) {
|
|
70
|
+
const options = { clients: [], yes: false, dryRun: false };
|
|
71
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
72
|
+
const arg = args[index];
|
|
73
|
+
if (arg === '--yes' || arg === '-y') {
|
|
74
|
+
options.yes = true;
|
|
75
|
+
} else if (arg === '--dry-run') {
|
|
76
|
+
options.dryRun = true;
|
|
77
|
+
} else if (arg === '--client' || arg.startsWith('--client=')) {
|
|
78
|
+
const value = arg === '--client' ? args[++index] : arg.slice('--client='.length);
|
|
79
|
+
const key = normalizeMcpInstallClient(value);
|
|
80
|
+
if (!key) {
|
|
81
|
+
throw new Error(`Unknown MCP client: ${value || '(missing)'}. Use claude, cursor, codex, vscode or antigravity.`);
|
|
82
|
+
}
|
|
83
|
+
if (!options.clients.includes(key)) {
|
|
84
|
+
options.clients.push(key);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`Unknown mcp install option: ${arg}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return options;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runMcpInstallCli(options) {
|
|
94
|
+
const ctx = createMcpInstallContext();
|
|
95
|
+
const warning = await prepareRuntimeLauncher(helperHome(), { dryRun: options.dryRun });
|
|
96
|
+
if (warning) {
|
|
97
|
+
console.error(warning);
|
|
98
|
+
}
|
|
99
|
+
const result = await runMcpInstall({ ...options, ctx, interactive: isInteractiveTerminal() });
|
|
100
|
+
console.log(result.output);
|
|
101
|
+
return result.results.some((item) => item.status === 'failed') ? 1 : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runMcpStatusCli() {
|
|
105
|
+
const result = await runMcpStatus({ ctx: createMcpInstallContext() });
|
|
106
|
+
console.log(result.output);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The configs point at <home>/bin/logwork-launch (macOS/Linux), which
|
|
111
|
+
// `logwork setup` writes. Write it here too, so `logwork mcp install` works
|
|
112
|
+
// without a fresh setup. Returns a warning when the runtime itself is missing.
|
|
113
|
+
export async function prepareRuntimeLauncher(home, {
|
|
114
|
+
dryRun = false,
|
|
115
|
+
platform = process.platform,
|
|
116
|
+
access = fs.access,
|
|
117
|
+
write = writeLauncher
|
|
118
|
+
} = {}) {
|
|
119
|
+
const entry = join(home, 'bin', 'logwork.mjs');
|
|
120
|
+
try {
|
|
121
|
+
await access(entry, fsConstants.F_OK);
|
|
122
|
+
} catch {
|
|
123
|
+
return `Warning: the logwork runtime is missing at ${entry}. Run \`logwork setup\` so the configured command can start.`;
|
|
124
|
+
}
|
|
125
|
+
if (!dryRun && platform !== 'win32') {
|
|
126
|
+
await write({ home, platform });
|
|
127
|
+
}
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isInteractiveTerminal() {
|
|
132
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function clackConfirm(message) {
|
|
136
|
+
const { confirm, isCancel } = await import('@clack/prompts');
|
|
137
|
+
const answer = await confirm({ message, initialValue: true });
|
|
138
|
+
return !isCancel(answer) && Boolean(answer);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Plans, asks (unless --yes), applies and formats. Returns
|
|
142
|
+
// { plans, results, output, pending } where pending lists the clients that
|
|
143
|
+
// still need a change when nothing was applied (dry run or not interactive).
|
|
144
|
+
export async function runMcpInstall({
|
|
145
|
+
ctx,
|
|
146
|
+
clients = [],
|
|
147
|
+
yes = false,
|
|
148
|
+
dryRun = false,
|
|
149
|
+
interactive = false,
|
|
150
|
+
confirmFn = clackConfirm
|
|
151
|
+
} = {}) {
|
|
152
|
+
const explicit = clients.length > 0;
|
|
153
|
+
const plans = await planMcpInstall(ctx, {
|
|
154
|
+
clients: explicit ? clients : MCP_INSTALL_CLIENT_KEYS,
|
|
155
|
+
explicit
|
|
156
|
+
});
|
|
157
|
+
const header = `MCP server "logwork" runs: ${formatCommandLine(ctx)}`;
|
|
158
|
+
const pending = plans.filter((plan) => plan.action === 'add' || plan.action === 'update');
|
|
159
|
+
|
|
160
|
+
if (!plans.length) {
|
|
161
|
+
return {
|
|
162
|
+
plans,
|
|
163
|
+
results: [],
|
|
164
|
+
pending,
|
|
165
|
+
output: [header, 'No MCP clients found (Claude Code, Cursor, Codex, VS Code, Google Antigravity). Use `/mcp` in the terminal UI for copy-ready configs.'].join('\n')
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (dryRun || (!yes && !interactive)) {
|
|
170
|
+
const lines = [header, dryRun ? 'Dry run: nothing was written.' : 'Not an interactive terminal: nothing was written.'];
|
|
171
|
+
for (const plan of plans) {
|
|
172
|
+
lines.push(...describeMcpPlan(plan, ctx));
|
|
173
|
+
}
|
|
174
|
+
if (pending.length && !dryRun) {
|
|
175
|
+
lines.push('Run `logwork mcp install --yes` to apply these changes.');
|
|
176
|
+
}
|
|
177
|
+
return { plans, results: [], pending, output: lines.join('\n') };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const plan of plans) {
|
|
182
|
+
if ((plan.action === 'add' || plan.action === 'update') && !yes) {
|
|
183
|
+
const verb = plan.action === 'add' ? 'Add' : 'Update';
|
|
184
|
+
const where = plan.kind === 'claude' && plan.cliPath ? 'with the claude CLI' : plan.file;
|
|
185
|
+
const accepted = await confirmFn(`${verb} the logwork MCP server in ${plan.label} (${where})?`);
|
|
186
|
+
if (!accepted) {
|
|
187
|
+
results.push({ key: plan.key, label: plan.label, file: plan.file, status: 'skipped' });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
results.push(await applyMcpClientPlan(plan, ctx));
|
|
192
|
+
}
|
|
193
|
+
return { plans, results, pending, output: [header, ...formatMcpInstallResults(results)].join('\n') };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatMcpInstallResults(results) {
|
|
197
|
+
const lines = [];
|
|
198
|
+
for (const result of results) {
|
|
199
|
+
switch (result.status) {
|
|
200
|
+
case 'added':
|
|
201
|
+
case 'updated':
|
|
202
|
+
lines.push(`- ${result.label}: ${result.status}${result.via ? ` (${result.via})` : ` (${result.file})`}${result.backup ? `; backup ${result.backup}` : ''}`);
|
|
203
|
+
break;
|
|
204
|
+
case 'up_to_date':
|
|
205
|
+
lines.push(`- ${result.label}: already up to date`);
|
|
206
|
+
break;
|
|
207
|
+
case 'skipped':
|
|
208
|
+
lines.push(`- ${result.label}: skipped`);
|
|
209
|
+
break;
|
|
210
|
+
case 'not_detected':
|
|
211
|
+
lines.push(`- ${result.label}: not detected; skipped`);
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
lines.push(`- ${result.label}: could not update automatically (${result.reason || 'unknown error'}). Add this manually${result.file ? ` to ${result.file}` : ''}:`);
|
|
215
|
+
lines.push(...String(result.snippet || '').split('\n').map((line) => ` ${line}`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (results.some((result) => result.status === 'added' || result.status === 'updated')) {
|
|
219
|
+
lines.push('Restart or reload the updated clients to pick up the change.');
|
|
220
|
+
}
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function runMcpStatus({ ctx } = {}) {
|
|
225
|
+
const clients = await inspectMcpClients(ctx);
|
|
226
|
+
const lines = [`MCP server "logwork" should run: ${formatCommandLine(ctx)}`];
|
|
227
|
+
const commandExists = await ctx.fs.access(ctx.launch.command).then(() => true, () => false);
|
|
228
|
+
if (!commandExists) {
|
|
229
|
+
lines.push(`Warning: ${ctx.launch.command} is missing, so these entries cannot start. Run \`logwork setup\`.`);
|
|
230
|
+
}
|
|
231
|
+
for (const client of clients) {
|
|
232
|
+
lines.push(...formatClientStatus(client));
|
|
233
|
+
}
|
|
234
|
+
const needsWork = clients.some((client) => client.detected && (client.status !== 'current' || client.legacy || (client.localScopes || []).length));
|
|
235
|
+
if (!clients.some((client) => client.detected)) {
|
|
236
|
+
lines.push('No MCP clients found. Use `/mcp` in the terminal UI for copy-ready configs.');
|
|
237
|
+
} else if (needsWork) {
|
|
238
|
+
lines.push('Run `logwork mcp install` to fix the entries above.');
|
|
239
|
+
}
|
|
240
|
+
return { clients, output: lines.join('\n') };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatClientStatus(client) {
|
|
244
|
+
if (!client.detected) {
|
|
245
|
+
return [`- ${client.label}: not detected`];
|
|
246
|
+
}
|
|
247
|
+
const where = client.kind === 'claude' && client.cliPath ? `${client.file}; claude CLI found` : client.file;
|
|
248
|
+
const state = {
|
|
249
|
+
current: 'current',
|
|
250
|
+
missing: 'missing',
|
|
251
|
+
stale: `stale: ${client.reason}`,
|
|
252
|
+
unreadable: `unreadable: ${client.reason}`
|
|
253
|
+
}[client.status] || client.status;
|
|
254
|
+
const lines = [`- ${client.label} (${where}): ${state}`];
|
|
255
|
+
if (client.legacy) {
|
|
256
|
+
lines.push(' leftover "logwork-helper" entry (old runtime)');
|
|
257
|
+
}
|
|
258
|
+
for (const stray of client.localScopes || []) {
|
|
259
|
+
lines.push(` local-scope "${stray.name}" entry for ${stray.project}: ${stray.reason || 'stale'}`);
|
|
260
|
+
}
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatCommandLine(ctx) {
|
|
265
|
+
return [ctx.launch.command, ...ctx.launch.argsPrefix, 'mcp'].join(' ');
|
|
266
|
+
}
|