logwork-cli 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -8
- package/README.md +41 -50
- package/RELEASE.md +3 -2
- package/bin/logwork.mjs +2 -2
- package/commands/commits.mjs +78 -14
- package/commands/mcp.mjs +266 -0
- package/commands/setup.mjs +161 -40
- package/docs/advanced.md +7 -6
- package/docs/mcp-setup.md +80 -28
- package/docs/operations-guide.md +18 -3
- package/docs/recipes.md +18 -5
- package/docs/security.md +20 -1
- 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/commit-workflow.mjs +160 -5
- package/lib/diagnostics.mjs +72 -9
- package/lib/launcher.mjs +303 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +34 -1
- package/lib/manual-ink-interaction.mjs +12 -0
- package/lib/manual-ink-ui.mjs +38 -0
- package/lib/manual-mcp.mjs +68 -22
- package/lib/manual-repl.mjs +147 -34
- package/lib/mcp-install.mjs +930 -0
- package/lib/mcp-server.mjs +2 -2
- package/lib/node-runtime.mjs +52 -12
- package/lib/project-mapping-workflow.mjs +2 -20
- package/lib/query-workflow.mjs +19 -0
- package/lib/reminder-scheduler.mjs +34 -18
- package/lib/reminder-service.mjs +18 -4
- package/lib/repo-project-suggestions.mjs +285 -0
- package/lib/ui.mjs +44 -0
- package/package.json +2 -2
- package/skills/logwork/SKILL.md +6 -1
package/LICENSE
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
4
|
-
All rights reserved.
|
|
3
|
+
Copyright (c) 2026 malcohelper and contributors
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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,13 +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
|
-
- **Project:** a project mapping whose `repos` list holds the repo wins
|
|
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.
|
|
130
|
+
|
|
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.
|
|
124
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.
|
|
125
133
|
|
|
126
|
-
Nothing is written. Paste the draft into `/logwork`, or let your AI agent use it.
|
|
134
|
+
Nothing is written to RO or Jira. Paste the draft into `/logwork`, or let your AI agent use it.
|
|
127
135
|
|
|
128
136
|
For agents:
|
|
129
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.
|
|
130
139
|
- **MCP prompt `log_from_commits`** gives an agent the full procedure:
|
|
131
140
|
1. find gaps with `get_logwork`;
|
|
132
141
|
2. draft from commits;
|
|
@@ -162,53 +171,35 @@ The default tasks shared by all projects are:
|
|
|
162
171
|
| 183 | Testing Activities | create |
|
|
163
172
|
| 111 | Other Activities | other |
|
|
164
173
|
|
|
165
|
-
## API Notes
|
|
166
|
-
|
|
167
|
-
- Read bookings: `GET /member-logtime/timesheet`
|
|
168
|
-
- Read worklog tasks: `GET /logwork/tasks?project_id=<id>` and `GET /logwork/tasks/defaults?project_id=<id>`
|
|
169
|
-
- Write an entry: `POST /logwork/entries`
|
|
170
|
-
- Delete an entry: `DELETE /logwork/entries/:id`
|
|
171
|
-
- Resubmit a rejected entry: `PATCH /logwork/entries/:id/resubmit`
|
|
172
|
-
|
|
173
|
-
```json
|
|
174
|
-
{
|
|
175
|
-
"worklog_task_id": 71,
|
|
176
|
-
"type_of_work": "create",
|
|
177
|
-
"effort_hours": 2,
|
|
178
|
-
"logdate": "2026-06-01",
|
|
179
|
-
"project_id": 720,
|
|
180
|
-
"description": "Implement maintenance mode management and status UI (SCB-213)"
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
`project_id` is sent only when the worklog task is a shared default task.
|
|
185
|
-
|
|
186
174
|
## Set Up MCP
|
|
187
175
|
|
|
188
|
-
`logwork setup`
|
|
176
|
+
Run `logwork setup` and answer yes for each client. It finds the clients you have and adds or updates the `logwork` server in each:
|
|
189
177
|
|
|
190
|
-
|
|
178
|
+
| Client | What setup changes |
|
|
179
|
+
|---|---|
|
|
180
|
+
| Claude Code | `claude mcp add --scope user logwork ...` (or `~/.claude.json` when the `claude` CLI is not on `PATH`) |
|
|
181
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
182
|
+
| Codex | `~/.codex/config.toml` |
|
|
183
|
+
| VS Code | user `mcp.json` (`~/Library/Application Support/Code/User` on macOS, `%APPDATA%\Code\User` on Windows) |
|
|
184
|
+
| Google Antigravity | `~/.gemini/antigravity/mcp_config.json` |
|
|
191
185
|
|
|
192
|
-
|
|
193
|
-
{
|
|
194
|
-
"mcpServers": {
|
|
195
|
-
"logwork": {
|
|
196
|
-
"command": "/Users/you/.nvm/versions/node/v24.1.0/bin/node",
|
|
197
|
-
"args": ["/Users/you/.logwork-cli/bin/logwork.mjs", "mcp"]
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
For Claude Code, run:
|
|
186
|
+
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.
|
|
204
187
|
|
|
205
188
|
```bash
|
|
206
|
-
|
|
189
|
+
logwork mcp status # which clients have a current logwork entry
|
|
190
|
+
logwork mcp install # add or update them later (asks per client)
|
|
191
|
+
logwork mcp install --dry-run # show the changes without writing
|
|
192
|
+
logwork setup --yes # update every detected client without asking
|
|
193
|
+
logwork setup --no-mcp-install
|
|
207
194
|
```
|
|
208
195
|
|
|
209
|
-
|
|
196
|
+
The configs never name a Node version, so Node upgrades need no config change:
|
|
197
|
+
- **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.
|
|
198
|
+
- **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`.
|
|
199
|
+
|
|
200
|
+
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.
|
|
210
201
|
|
|
211
|
-
Restart or reload the IDE's MCP tools after
|
|
202
|
+
Restart or reload the IDE's MCP tools after a change.
|
|
212
203
|
|
|
213
204
|
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**:
|
|
214
205
|
1. Call the tool without `confirm`. It returns a preview with a `batchId` or `previewId` and a summary.
|
|
@@ -241,11 +232,11 @@ Options:
|
|
|
241
232
|
|
|
242
233
|
The long forms `--task-name` and `--description` also work. Project and date are always preserved.
|
|
243
234
|
|
|
244
|
-
Resource Optimiser
|
|
235
|
+
Resource Optimiser only lets project managers edit an entry. So apply **replaces** the entry:
|
|
245
236
|
|
|
246
237
|
1. It re-reads the entry and stops if the entry changed after the preview.
|
|
247
|
-
2. It creates the corrected entry
|
|
248
|
-
3. It deletes the original
|
|
238
|
+
2. It creates the corrected entry.
|
|
239
|
+
3. It deletes the original.
|
|
249
240
|
4. It verifies the new entry.
|
|
250
241
|
|
|
251
242
|
If the create fails, the original is untouched. If the delete fails, the new entry is deleted again, so nothing changes.
|
|
@@ -292,7 +283,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
|
|
|
292
283
|
- `/query this-week`
|
|
293
284
|
- `/reconcile`
|
|
294
285
|
- `/projects`
|
|
295
|
-
- `/map
|
|
286
|
+
- `/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)
|
|
296
287
|
- `/history`
|
|
297
288
|
- `/diagnostics`
|
|
298
289
|
|
|
@@ -321,7 +312,7 @@ Mở Terminal và chạy: logwork commits today (bản nháp từ commit)
|
|
|
321
312
|
|
|
322
313
|
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.
|
|
323
314
|
|
|
324
|
-
macOS uses a user `launchd` agent (`sg.vinova.logwork-cli.reminder`) and Windows uses Task Scheduler (`Logwork Reminder`). Neither schedule stores credentials.
|
|
315
|
+
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.
|
|
325
316
|
|
|
326
317
|
## Reconcile RO And Jira
|
|
327
318
|
|
|
@@ -354,4 +345,4 @@ Updating keeps OS credentials, project mappings, drafts and apply history. Recon
|
|
|
354
345
|
|
|
355
346
|
## License
|
|
356
347
|
|
|
357
|
-
|
|
348
|
+
[MIT](LICENSE) © 2026 malcohelper and contributors.
|
package/RELEASE.md
CHANGED
|
@@ -64,8 +64,9 @@ The gate must pass all of the following:
|
|
|
64
64
|
## Manual Verification
|
|
65
65
|
|
|
66
66
|
- Install from the packed tarball in a temporary directory.
|
|
67
|
-
- Run `logwork setup --no-login
|
|
68
|
-
- 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.
|
|
69
70
|
- Run `logwork --help` and `logwork --version`.
|
|
70
71
|
- Run `logwork update check --force` against the published npm version.
|
|
71
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,14 +1,18 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
|
|
4
|
+
import { upsertProjectMapping } from '../lib/project-mapping-workflow.mjs';
|
|
4
5
|
import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
|
|
5
6
|
import { loadReminderConfig } from '../lib/reminder-config.mjs';
|
|
6
7
|
import { collectReminderRepos } from '../lib/reminder-workflow.mjs';
|
|
8
|
+
import { chooseRepoProject } from '../lib/ui.mjs';
|
|
7
9
|
|
|
8
|
-
// `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
|
|
9
|
-
// 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.
|
|
10
14
|
export function parseCommitsArgs(args = []) {
|
|
11
|
-
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 };
|
|
12
16
|
const positional = [];
|
|
13
17
|
for (let index = 0; index < args.length; index += 1) {
|
|
14
18
|
const arg = args[index];
|
|
@@ -18,6 +22,8 @@ export function parseCommitsArgs(args = []) {
|
|
|
18
22
|
options.json = true;
|
|
19
23
|
} else if (arg === '--no-bookings') {
|
|
20
24
|
options.useBookings = false;
|
|
25
|
+
} else if (arg === '--no-prompt') {
|
|
26
|
+
options.prompt = false;
|
|
21
27
|
} else if (arg === '--repo' || arg === '--author') {
|
|
22
28
|
const value = args[index + 1];
|
|
23
29
|
if (!value || value.startsWith('--')) {
|
|
@@ -52,7 +58,10 @@ export async function run(args = [], {
|
|
|
52
58
|
draft = draftLogworkFromCommits,
|
|
53
59
|
print = console.log,
|
|
54
60
|
cwd = process.cwd(),
|
|
55
|
-
reminderRepos = loadReminderRepos
|
|
61
|
+
reminderRepos = loadReminderRepos,
|
|
62
|
+
isInteractive = defaultIsInteractive,
|
|
63
|
+
chooseProject = chooseRepoProject,
|
|
64
|
+
saveMapping = upsertProjectMapping
|
|
56
65
|
} = {}) {
|
|
57
66
|
const options = parseCommitsArgs(args);
|
|
58
67
|
if (options.help) {
|
|
@@ -71,20 +80,70 @@ export async function run(args = [], {
|
|
|
71
80
|
fallbackNote = `Not in a git repo; using the reminder repos: ${fallback.join(', ')}`;
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
|
-
const
|
|
83
|
+
const draftOptions = {
|
|
75
84
|
period: options.period,
|
|
76
85
|
repos,
|
|
77
86
|
author: options.author,
|
|
78
87
|
useBookings: options.useBookings
|
|
79
|
-
}
|
|
88
|
+
};
|
|
89
|
+
const result = await draft(draftOptions);
|
|
80
90
|
if (options.json) {
|
|
81
91
|
print(JSON.stringify(result, null, 2));
|
|
82
|
-
|
|
83
|
-
|
|
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}`);
|
|
84
103
|
}
|
|
85
104
|
return 0;
|
|
86
105
|
}
|
|
87
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
|
+
|
|
88
147
|
// True when dir or a parent holds .git (a directory, or a file for worktrees
|
|
89
148
|
// and submodules). No git process is started.
|
|
90
149
|
export function isInsideGitWorkTree(dir, { exists = existsSync } = {}) {
|
|
@@ -108,7 +167,8 @@ async function loadReminderRepos() {
|
|
|
108
167
|
export function formatCommitsHelp() {
|
|
109
168
|
return `Usage:
|
|
110
169
|
logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
|
|
111
|
-
[--repo <path>]... [--author <email>] [--no-bookings]
|
|
170
|
+
[--repo <path>]... [--author <email>] [--no-bookings]
|
|
171
|
+
[--no-prompt] [--json]
|
|
112
172
|
|
|
113
173
|
Reads your commits (all branches, your git user.email) from each repo (default:
|
|
114
174
|
the current directory; outside a git repo, the repos set with
|
|
@@ -120,14 +180,18 @@ the current directory; outside a git repo, the repos set with
|
|
|
120
180
|
fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
|
|
121
181
|
test -> Testing Activities, ci/build/release -> Deployment & Release,
|
|
122
182
|
docs/design -> Requirements & Design, chore -> Other Activities.
|
|
123
|
-
- Each group gets an RO project: a mapping whose repos holds the repo
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
126
190
|
- Each project's RO remaining time (booked - logged) is split across its
|
|
127
191
|
groups by how much work each holds; groups without a project share the room
|
|
128
192
|
of booked projects no group claimed. With --no-bookings or without login, a
|
|
129
193
|
weekday is 8h split across all groups.
|
|
130
194
|
|
|
131
|
-
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
|
|
132
196
|
to your AI agent (log_work).`;
|
|
133
197
|
}
|
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
|
+
}
|