logwork-cli 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -0
- package/README.md +330 -2
- package/RELEASE.md +89 -0
- package/bin/logwork.mjs +104 -0
- package/commands/auth.mjs +227 -0
- package/commands/commits.mjs +82 -0
- package/commands/delete.mjs +69 -0
- package/commands/doctor.mjs +24 -0
- package/commands/edit.mjs +88 -0
- package/commands/history.mjs +62 -0
- package/commands/query.mjs +52 -0
- package/commands/quick.mjs +163 -0
- package/commands/reconcile.mjs +55 -0
- package/commands/release.mjs +77 -0
- package/commands/reminder.mjs +172 -0
- package/commands/setup.mjs +508 -0
- package/commands/skill.mjs +91 -0
- package/commands/tui.mjs +50 -0
- package/commands/update.mjs +161 -0
- package/docs/advanced.md +334 -0
- package/docs/mcp-setup.md +368 -0
- package/docs/operations-guide.md +587 -0
- package/docs/recipes.md +259 -0
- package/docs/security.md +151 -0
- package/examples/mcp/antigravity.mcp_config.json +8 -0
- package/examples/mcp/codex.config.toml +5 -0
- package/examples/mcp/copilot.mcp.json +9 -0
- package/lib/agent-skill.mjs +16 -0
- package/lib/api-auth.mjs +1259 -0
- package/lib/api.mjs +1422 -0
- package/lib/apply-ledger.mjs +227 -0
- package/lib/atomic-file.mjs +24 -0
- package/lib/auth-diagnostics.mjs +113 -0
- package/lib/auth-errors.mjs +22 -0
- package/lib/auth-redaction.mjs +78 -0
- package/lib/auth-terminal.mjs +97 -0
- package/lib/auth.mjs +323 -0
- package/lib/batch-parser.mjs +145 -0
- package/lib/batch-workflow.mjs +842 -0
- package/lib/commit-workflow.mjs +402 -0
- package/lib/config.mjs +191 -0
- package/lib/credential-store.mjs +130 -0
- package/lib/diagnostics.mjs +307 -0
- package/lib/entrypoint.mjs +14 -0
- package/lib/file-lock.mjs +44 -0
- package/lib/http.mjs +125 -0
- package/lib/jira-api.mjs +258 -0
- package/lib/jira-auth.mjs +203 -0
- package/lib/jira-workflow.mjs +523 -0
- package/lib/keychain.mjs +105 -0
- package/lib/legacy-migration.mjs +66 -0
- package/lib/list-navigation.mjs +19 -0
- package/lib/logwork-config.mjs +199 -0
- package/lib/logwork-f03-enrich.mjs +41 -0
- package/lib/macos.mjs +33 -0
- package/lib/manual-apply-state.mjs +35 -0
- package/lib/manual-commands.mjs +294 -0
- package/lib/manual-drafts.mjs +154 -0
- package/lib/manual-ink-app.mjs +1654 -0
- package/lib/manual-ink-auth.mjs +349 -0
- package/lib/manual-ink-ui.mjs +675 -0
- package/lib/manual-input.mjs +21 -0
- package/lib/manual-logwork-wizard.mjs +310 -0
- package/lib/manual-mcp.mjs +187 -0
- package/lib/manual-repl.mjs +1037 -0
- package/lib/manual-targets.mjs +78 -0
- package/lib/mcp-server.mjs +833 -0
- package/lib/os-notification.mjs +105 -0
- package/lib/package-info.mjs +17 -0
- package/lib/paths.mjs +68 -0
- package/lib/project-identity.mjs +66 -0
- package/lib/project-mapping-workflow.mjs +309 -0
- package/lib/project-resolver.mjs +373 -0
- package/lib/query-workflow.mjs +553 -0
- package/lib/reconciliation-workflow.mjs +446 -0
- package/lib/release-doctor.mjs +448 -0
- package/lib/reminder-config.mjs +71 -0
- package/lib/reminder-scheduler.mjs +301 -0
- package/lib/reminder-service.mjs +118 -0
- package/lib/reminder-state.mjs +38 -0
- package/lib/reminder-workflow.mjs +299 -0
- package/lib/ro-delete-args.mjs +38 -0
- package/lib/ro-delete-workflow.mjs +115 -0
- package/lib/ro-edit-args.mjs +142 -0
- package/lib/ro-edit-workflow.mjs +632 -0
- package/lib/ro-entry-reader.mjs +56 -0
- package/lib/ro-resubmit-workflow.mjs +134 -0
- package/lib/terminal-auth-prompts.mjs +120 -0
- package/lib/ui.mjs +133 -0
- package/lib/update-service.mjs +350 -0
- package/lib/util.mjs +66 -0
- package/lib/windows-credential-manager.mjs +260 -0
- package/lib/worklog-task-resolver.mjs +113 -0
- package/package.json +73 -4
- package/setup.sh +22 -0
- package/skills/logwork/SKILL.md +43 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
projectIdentityKey,
|
|
3
|
+
projectMatchesMapping
|
|
4
|
+
} from './project-identity.mjs';
|
|
5
|
+
|
|
6
|
+
const RESOLVE_THRESHOLD = 0.75;
|
|
7
|
+
const AMBIGUOUS_MARGIN = 0.1;
|
|
8
|
+
const CONFIG_TICKET_CONFIDENCE = 0.99;
|
|
9
|
+
const CONFIG_KEYWORD_CONFIDENCE = 0.98;
|
|
10
|
+
const PROJECT_NAME_CONFIDENCE = 0.95;
|
|
11
|
+
const PROJECT_NAME_TOKEN_CONFIDENCE = 0.65;
|
|
12
|
+
const SINGLE_BOOKED_CONFIDENCE = 0.95;
|
|
13
|
+
|
|
14
|
+
export function resolveEntryProject({
|
|
15
|
+
entry,
|
|
16
|
+
projects,
|
|
17
|
+
membershipProjects = [],
|
|
18
|
+
projectOverrides = {},
|
|
19
|
+
config = {}
|
|
20
|
+
}) {
|
|
21
|
+
const bookedProjects = Array.isArray(projects) ? projects : [];
|
|
22
|
+
const memberships = Array.isArray(membershipProjects) ? membershipProjects : [];
|
|
23
|
+
const override = projectOverrides?.[entry.id];
|
|
24
|
+
|
|
25
|
+
if (override !== undefined && override !== null && String(override).trim() !== '') {
|
|
26
|
+
const project = findProjectByMemberId(bookedProjects, override);
|
|
27
|
+
|
|
28
|
+
if (project) {
|
|
29
|
+
return resolved(project, 1, 'override', [], true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const membership = findProjectByMemberId(memberships, override);
|
|
33
|
+
if (membership) {
|
|
34
|
+
return resolved(membership, 1, 'override_unbooked', [], false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return unresolved('override_not_found', combineCandidates(bookedProjects, memberships));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const membershipCandidates = memberships.filter((membership) => (
|
|
41
|
+
!bookedProjects.some((booked) => String(booked.projectMemberId) === String(membership.projectMemberId))
|
|
42
|
+
));
|
|
43
|
+
|
|
44
|
+
if (bookedProjects.length === 1) {
|
|
45
|
+
const bookedProject = bookedProjects[0];
|
|
46
|
+
const bookedScore = scoreProject(entry, bookedProject, config);
|
|
47
|
+
|
|
48
|
+
if (isConfigSource(bookedScore.source)) {
|
|
49
|
+
return resolved(bookedProject, bookedScore.confidence, bookedScore.source, [bookedScore], true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const conflicts = findConflictingMappingCandidates({
|
|
53
|
+
entry,
|
|
54
|
+
bookedProject,
|
|
55
|
+
membershipCandidates,
|
|
56
|
+
config
|
|
57
|
+
});
|
|
58
|
+
if (conflicts.length) {
|
|
59
|
+
return unresolvedWithCandidates(
|
|
60
|
+
'single_booked_project_conflicts_with_mapping',
|
|
61
|
+
[
|
|
62
|
+
...conflicts,
|
|
63
|
+
toCandidate({
|
|
64
|
+
project: bookedProject,
|
|
65
|
+
confidence: SINGLE_BOOKED_CONFIDENCE,
|
|
66
|
+
source: 'single_booked_project'
|
|
67
|
+
}, true)
|
|
68
|
+
],
|
|
69
|
+
conflicts[0].confidence
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return resolved(bookedProject, SINGLE_BOOKED_CONFIDENCE, 'single_booked_project', [], true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (bookedProjects.length > 1) {
|
|
77
|
+
const bookedResult = resolveScoredProjects(entry, bookedProjects, config, true);
|
|
78
|
+
if (bookedResult.status === 'resolved') {
|
|
79
|
+
return bookedResult;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const unbookedResult = resolveScoredProjects(entry, membershipCandidates, config, false);
|
|
84
|
+
if (unbookedResult.status === 'resolved_unbooked') {
|
|
85
|
+
return unbookedResult;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (bookedProjects.length === 0) {
|
|
89
|
+
return unresolved('no_booked_projects', membershipCandidates);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const scored = bookedProjects
|
|
93
|
+
.map((project) => scoreProject(entry, project, config))
|
|
94
|
+
.sort((left, right) => right.confidence - left.confidence);
|
|
95
|
+
const best = scored[0];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
status: 'unresolved',
|
|
99
|
+
reason: best.confidence > 0 ? 'ambiguous_project_match' : 'no_project_match',
|
|
100
|
+
confidence: best.confidence,
|
|
101
|
+
candidates: scored.map((candidate) => toCandidate(candidate, true))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveScoredProjects(entry, projects, config, booked) {
|
|
106
|
+
if (!projects.length) {
|
|
107
|
+
return unresolved(booked ? 'no_booked_projects' : 'no_membership_project_match', []);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const scored = projects
|
|
111
|
+
.map((project) => scoreProject(entry, project, config, { allowProjectTextMatch: booked }))
|
|
112
|
+
.sort((left, right) => right.confidence - left.confidence);
|
|
113
|
+
const best = scored[0];
|
|
114
|
+
const second = scored[1];
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
best.confidence >= RESOLVE_THRESHOLD &&
|
|
118
|
+
isClearWinner(best, second)
|
|
119
|
+
) {
|
|
120
|
+
return resolved(best.project, best.confidence, booked ? best.source : `${best.source}_unbooked`, scored, booked);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
status: 'unresolved',
|
|
125
|
+
reason: best.confidence > 0 ? 'ambiguous_project_match' : 'no_project_match',
|
|
126
|
+
confidence: best.confidence,
|
|
127
|
+
candidates: scored.map((candidate) => toCandidate(candidate, booked))
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scoreProject(entry, project, config, { allowProjectTextMatch = true } = {}) {
|
|
132
|
+
const taskText = normalize(entry.text ?? entry.taskName);
|
|
133
|
+
const projectName = normalize(project.projectName);
|
|
134
|
+
let confidence = 0;
|
|
135
|
+
let source = 'none';
|
|
136
|
+
|
|
137
|
+
if (allowProjectTextMatch && projectName && taskText.includes(projectName)) {
|
|
138
|
+
confidence = PROJECT_NAME_CONFIDENCE;
|
|
139
|
+
source = 'project_name';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (allowProjectTextMatch) {
|
|
143
|
+
for (const word of projectName.split(/\s+/).filter((part) => part.length >= 4)) {
|
|
144
|
+
if (taskText.includes(word) && confidence < PROJECT_NAME_TOKEN_CONFIDENCE) {
|
|
145
|
+
confidence = PROJECT_NAME_TOKEN_CONFIDENCE;
|
|
146
|
+
source = 'project_name_token';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const mapping of config.projectMappings || []) {
|
|
152
|
+
if (!mappingMatchesProject(mapping, project)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mappingMatch = matchEntryMapping(entry, mapping);
|
|
157
|
+
if (mappingMatch && confidence < mappingMatch.confidence) {
|
|
158
|
+
confidence = mappingMatch.confidence;
|
|
159
|
+
source = mappingMatch.source;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { project, confidence, source };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mappingMatchesProject(mapping, project) {
|
|
167
|
+
return projectMatchesMapping(project, mapping);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findConflictingMappingCandidates({
|
|
171
|
+
entry,
|
|
172
|
+
bookedProject,
|
|
173
|
+
membershipCandidates,
|
|
174
|
+
config
|
|
175
|
+
}) {
|
|
176
|
+
const candidates = new Map();
|
|
177
|
+
|
|
178
|
+
for (const mapping of config.projectMappings || []) {
|
|
179
|
+
const mappingMatch = matchEntryMapping(entry, mapping);
|
|
180
|
+
if (!mappingMatch || mappingMatchesProject(mapping, bookedProject)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const matchedMemberships = membershipCandidates.filter((membership) => mappingMatchesProject(mapping, membership));
|
|
185
|
+
if (matchedMemberships.length) {
|
|
186
|
+
for (const membership of matchedMemberships) {
|
|
187
|
+
addCandidate(candidates, {
|
|
188
|
+
project: membership,
|
|
189
|
+
booked: false,
|
|
190
|
+
confidence: mappingMatch.confidence,
|
|
191
|
+
source: mappingMatch.source
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const mappedProject = projectFromMapping(mapping);
|
|
198
|
+
if (mappedProject) {
|
|
199
|
+
addCandidate(candidates, {
|
|
200
|
+
project: mappedProject,
|
|
201
|
+
booked: false,
|
|
202
|
+
confidence: mappingMatch.confidence,
|
|
203
|
+
source: mappingMatch.source
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return [...candidates.values()]
|
|
209
|
+
.map((candidate) => toCandidate(candidate, candidate.booked))
|
|
210
|
+
.sort(compareCandidates);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function matchEntryMapping(entry, mapping) {
|
|
214
|
+
const ticketMatch = (entry.tickets || []).some((ticket) => (
|
|
215
|
+
(mapping.tickets || []).some((prefix) => {
|
|
216
|
+
const normalizedPrefix = String(prefix || '').trim().toUpperCase();
|
|
217
|
+
return normalizedPrefix && ticket.startsWith(normalizedPrefix);
|
|
218
|
+
})
|
|
219
|
+
));
|
|
220
|
+
if (ticketMatch) {
|
|
221
|
+
return {
|
|
222
|
+
confidence: CONFIG_TICKET_CONFIDENCE,
|
|
223
|
+
source: 'config_ticket'
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const taskText = normalize(entry.text ?? entry.taskName);
|
|
228
|
+
const keywordMatch = (mapping.keywords || []).some((keyword) => {
|
|
229
|
+
const normalizedKeyword = normalize(keyword);
|
|
230
|
+
return normalizedKeyword && taskText.includes(normalizedKeyword);
|
|
231
|
+
});
|
|
232
|
+
if (keywordMatch) {
|
|
233
|
+
return {
|
|
234
|
+
confidence: CONFIG_KEYWORD_CONFIDENCE,
|
|
235
|
+
source: 'config_keyword'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function projectFromMapping(mapping) {
|
|
243
|
+
const project = {
|
|
244
|
+
projectMemberId: mapping.projectMemberId,
|
|
245
|
+
projectId: mapping.projectId,
|
|
246
|
+
projectName: mapping.projectName
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const hasProjectData = project.projectMemberId !== undefined ||
|
|
250
|
+
project.projectId !== undefined ||
|
|
251
|
+
Boolean(normalize(project.projectName));
|
|
252
|
+
|
|
253
|
+
return hasProjectData
|
|
254
|
+
? project
|
|
255
|
+
: null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function addCandidate(candidates, candidate) {
|
|
259
|
+
const key = candidateKey(candidate.project);
|
|
260
|
+
const existing = candidates.get(key);
|
|
261
|
+
if (!existing || candidate.confidence > existing.confidence) {
|
|
262
|
+
candidates.set(key, candidate);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function unresolvedWithCandidates(reason, candidates, confidence = 0) {
|
|
267
|
+
return {
|
|
268
|
+
status: 'unresolved',
|
|
269
|
+
reason,
|
|
270
|
+
confidence,
|
|
271
|
+
candidates
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isClearWinner(best, second) {
|
|
276
|
+
if (!second) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (isConfigSource(best.source) && !isConfigSource(second.source)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return best.confidence - second.confidence >= AMBIGUOUS_MARGIN;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isConfigSource(source) {
|
|
288
|
+
return source === 'config_ticket' || source === 'config_keyword';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolved(project, confidence, source, candidates, booked) {
|
|
292
|
+
return {
|
|
293
|
+
status: booked ? 'resolved' : 'resolved_unbooked',
|
|
294
|
+
reason: source,
|
|
295
|
+
confidence,
|
|
296
|
+
booked,
|
|
297
|
+
requiresAllowUnbooked: !booked,
|
|
298
|
+
project,
|
|
299
|
+
candidates: candidates.map((candidate) => toCandidate(candidate, booked))
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function unresolved(reason, projects) {
|
|
304
|
+
return {
|
|
305
|
+
status: 'unresolved',
|
|
306
|
+
reason,
|
|
307
|
+
confidence: 0,
|
|
308
|
+
candidates: projects.map((project) => ({
|
|
309
|
+
projectMemberId: project.projectMemberId,
|
|
310
|
+
projectId: project.projectId,
|
|
311
|
+
projectName: project.projectName,
|
|
312
|
+
booked: Boolean(project.booked),
|
|
313
|
+
confidence: 0,
|
|
314
|
+
source: 'none'
|
|
315
|
+
}))
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function combineCandidates(bookedProjects, membershipProjects) {
|
|
320
|
+
const candidates = new Map();
|
|
321
|
+
|
|
322
|
+
for (const project of bookedProjects) {
|
|
323
|
+
candidates.set(candidateKey(project), {
|
|
324
|
+
...project,
|
|
325
|
+
booked: true
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const project of membershipProjects) {
|
|
330
|
+
const key = candidateKey(project);
|
|
331
|
+
if (candidates.has(key)) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
candidates.set(key, {
|
|
336
|
+
...project,
|
|
337
|
+
booked: false
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return [...candidates.values()];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function toCandidate(item, booked = item.booked) {
|
|
345
|
+
const project = item.project || item;
|
|
346
|
+
return {
|
|
347
|
+
projectMemberId: project.projectMemberId,
|
|
348
|
+
projectId: project.projectId,
|
|
349
|
+
projectName: project.projectName,
|
|
350
|
+
booked: Boolean(booked),
|
|
351
|
+
confidence: item.confidence ?? 0,
|
|
352
|
+
source: item.source || 'none'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function compareCandidates(left, right) {
|
|
357
|
+
return right.confidence - left.confidence ||
|
|
358
|
+
String(left.projectName || '').localeCompare(String(right.projectName || ''));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function findProjectByMemberId(projects, projectMemberId) {
|
|
362
|
+
return projects.find((candidate) => (
|
|
363
|
+
String(candidate.projectMemberId) === String(projectMemberId)
|
|
364
|
+
));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function candidateKey(project) {
|
|
368
|
+
return projectIdentityKey(project);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalize(value) {
|
|
372
|
+
return String(value || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
373
|
+
}
|