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
|
@@ -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
|
+
}
|