logwork-cli 1.1.1 → 1.2.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.
Files changed (64) hide show
  1. package/README.md +23 -7
  2. package/RELEASE.md +1 -0
  3. package/commands/commits.mjs +57 -6
  4. package/commands/history.mjs +0 -4
  5. package/commands/quick.mjs +38 -23
  6. package/commands/reminder.mjs +41 -2
  7. package/commands/setup.mjs +50 -16
  8. package/commands/tui.mjs +1 -0
  9. package/docs/advanced.md +12 -2
  10. package/docs/mcp-setup.md +56 -14
  11. package/docs/operations-guide.md +35 -2
  12. package/docs/recipes.md +27 -0
  13. package/docs/security.md +3 -1
  14. package/examples/mcp/antigravity.mcp_config.json +1 -1
  15. package/examples/mcp/codex.config.toml +1 -1
  16. package/examples/mcp/copilot.mcp.json +1 -1
  17. package/lib/api-auth-forms.mjs +393 -0
  18. package/lib/api-auth-http.mjs +184 -0
  19. package/lib/api-auth-support.mjs +101 -0
  20. package/lib/api-auth-tokens.mjs +129 -0
  21. package/lib/api-auth.mjs +39 -813
  22. package/lib/api.mjs +38 -1422
  23. package/lib/apply-ledger.mjs +1 -1
  24. package/lib/batch-workflow.mjs +187 -2
  25. package/lib/commit-workflow.mjs +188 -21
  26. package/lib/config.mjs +7 -0
  27. package/lib/diagnostics.mjs +53 -4
  28. package/lib/jira-api.mjs +25 -3
  29. package/lib/jira-workflow.mjs +36 -6
  30. package/lib/logwork-config.mjs +64 -13
  31. package/lib/manual-apply-state.mjs +15 -0
  32. package/lib/manual-commands.mjs +2 -2
  33. package/lib/manual-ink-app.mjs +108 -1281
  34. package/lib/manual-ink-draft-actions.mjs +227 -0
  35. package/lib/manual-ink-interaction.mjs +306 -0
  36. package/lib/manual-ink-session.mjs +41 -0
  37. package/lib/manual-ink-task-editor.mjs +338 -0
  38. package/lib/manual-ink-wizard.mjs +520 -0
  39. package/lib/manual-mcp.mjs +27 -15
  40. package/lib/manual-repl.mjs +39 -3
  41. package/lib/mcp-server.mjs +51 -20
  42. package/lib/node-runtime.mjs +115 -0
  43. package/lib/paths.mjs +7 -0
  44. package/lib/project-mapping-workflow.mjs +30 -7
  45. package/lib/query-workflow.mjs +0 -1
  46. package/lib/reconciliation-workflow.mjs +1 -1
  47. package/lib/reminder-config.mjs +38 -2
  48. package/lib/reminder-scheduler.mjs +54 -3
  49. package/lib/reminder-service.mjs +28 -3
  50. package/lib/reminder-workflow.mjs +118 -22
  51. package/lib/ro-api-diagnostics.mjs +109 -0
  52. package/lib/ro-api-entries.mjs +359 -0
  53. package/lib/ro-api-fields.mjs +290 -0
  54. package/lib/ro-api-http.mjs +68 -0
  55. package/lib/ro-api-projects.mjs +137 -0
  56. package/lib/ro-api-tasks.mjs +50 -0
  57. package/lib/ro-api-timesheet.mjs +413 -0
  58. package/lib/ro-delete-workflow.mjs +2 -1
  59. package/lib/ro-entry-reader.mjs +11 -6
  60. package/lib/ro-resubmit-workflow.mjs +2 -1
  61. package/lib/terminal-auth-prompts.mjs +7 -4
  62. package/lib/ui.mjs +80 -59
  63. package/package.json +11 -4
  64. package/skills/logwork/SKILL.md +5 -4
@@ -0,0 +1,129 @@
1
+ // Token extraction from Resource Optimiser and Keycloak responses: picks the RO
2
+ // JWT (not the Keycloak one) and lists JSON key paths for sanitized diagnostics.
3
+
4
+ export function extractRoToken(data, headers) {
5
+ return extractRoSession(data, headers)?.accessToken || null;
6
+ }
7
+
8
+ export function extractRoSession(data, headers) {
9
+ const accessToken = extractRoAccessToken(data, headers);
10
+ if (!accessToken) {
11
+ return null;
12
+ }
13
+ return {
14
+ accessToken,
15
+ refreshToken: extractRoRefreshToken(data)
16
+ };
17
+ }
18
+
19
+ function extractRoAccessToken(data, headers) {
20
+ const candidates = [
21
+ readBearerHeader(headers),
22
+ ...collectJwtCandidatesByKey(data, isAccessTokenKey)
23
+ ].filter(Boolean);
24
+ return candidates.find(isResourceOptimiserJwt) || null;
25
+ }
26
+
27
+ function extractRoRefreshToken(data) {
28
+ const candidates = collectJwtCandidatesByKey(data, isRefreshTokenKey).filter(Boolean);
29
+ return candidates.find(isResourceOptimiserJwt) || null;
30
+ }
31
+
32
+ function readBearerHeader(headers) {
33
+ const value = headers?.get?.('authorization') || headers?.get?.('Authorization') || '';
34
+ const match = String(value).match(/^Bearer\s+(.+)$/i);
35
+ return match?.[1]?.trim() || null;
36
+ }
37
+
38
+ function collectJwtCandidatesByKey(value, keyMatcher, seen = new Set()) {
39
+ if (!value || typeof value !== 'object' || seen.has(value)) {
40
+ return [];
41
+ }
42
+ seen.add(value);
43
+
44
+ const candidates = [];
45
+ if (Array.isArray(value)) {
46
+ for (const item of value) {
47
+ candidates.push(...collectJwtCandidatesByKey(item, keyMatcher, seen));
48
+ }
49
+ return candidates;
50
+ }
51
+
52
+ for (const [key, item] of Object.entries(value)) {
53
+ if (typeof item === 'string' && keyMatcher(key) && item.split('.').length >= 3) {
54
+ candidates.push(item);
55
+ }
56
+ candidates.push(...collectJwtCandidatesByKey(item, keyMatcher, seen));
57
+ }
58
+ return candidates;
59
+ }
60
+
61
+ function isAccessTokenKey(key) {
62
+ return /^(access[_-]?token|token|jwt)$/i.test(String(key || ''));
63
+ }
64
+
65
+ function isRefreshTokenKey(key) {
66
+ return /^refresh[_-]?token$/i.test(String(key || ''));
67
+ }
68
+
69
+ export function collectJsonKeyPaths(value, path = '', seen = new Set(), output = []) {
70
+ if (!value || typeof value !== 'object' || seen.has(value)) {
71
+ return output;
72
+ }
73
+ seen.add(value);
74
+
75
+ if (Array.isArray(value)) {
76
+ value.slice(0, 5).forEach((item, index) => {
77
+ collectJsonKeyPaths(item, `${path}[${index}]`, seen, output);
78
+ });
79
+ return output;
80
+ }
81
+
82
+ for (const key of Object.keys(value)) {
83
+ const nextPath = path ? `${path}.${key}` : key;
84
+ output.push(nextPath);
85
+ if (output.length >= 40) {
86
+ return output;
87
+ }
88
+ collectJsonKeyPaths(value[key], nextPath, seen, output);
89
+ }
90
+ return output;
91
+ }
92
+
93
+ function isResourceOptimiserJwt(token) {
94
+ const payload = decodeJwtPayload(token);
95
+ if (!payload) {
96
+ return false;
97
+ }
98
+ if (isKeycloakJwtPayload(payload)) {
99
+ return false;
100
+ }
101
+ return Boolean(
102
+ payload.id !== undefined
103
+ || typeof payload.email === 'string'
104
+ || typeof payload.db_url === 'string'
105
+ || typeof payload.dbUrl === 'string'
106
+ || typeof payload.user_id === 'number'
107
+ || typeof payload.userId === 'number'
108
+ );
109
+ }
110
+
111
+ function isKeycloakJwtPayload(payload) {
112
+ const issuer = String(payload.iss || '').toLowerCase();
113
+ return issuer.includes('keycloak')
114
+ || payload.azp === 'localhost'
115
+ || payload.realm_access !== undefined
116
+ || payload.resource_access !== undefined;
117
+ }
118
+
119
+ export function decodeJwtPayload(token) {
120
+ const [, payload] = String(token || '').split('.');
121
+ if (!payload) {
122
+ return null;
123
+ }
124
+ try {
125
+ return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
126
+ } catch {
127
+ return null;
128
+ }
129
+ }