fraim 2.0.226 → 2.0.228
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.
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Handoff Contract Validation (Issue #916)
|
|
4
|
+
*
|
|
5
|
+
* Pure helpers for validating the three evidence fields that FRAIM jobs must
|
|
6
|
+
* emit on key seekMentoring calls so the Hub can render its review surfaces:
|
|
7
|
+
*
|
|
8
|
+
* - evidence.reviewHandoff (submit phase, status === "awaiting_mentor")
|
|
9
|
+
* - evidence.nextJobRecommendations (retrospective phase completion)
|
|
10
|
+
* - evidence.delegationLedger (create-delegation-graph phase)
|
|
11
|
+
*
|
|
12
|
+
* Lives in src/core so both the local MCP proxy and evals can import the same
|
|
13
|
+
* contract without cross-layer dependencies — same pattern as quality-evidence.ts.
|
|
14
|
+
*
|
|
15
|
+
* Primary call site: src/local-mcp-server/stdio-server.ts seekMentoring handler,
|
|
16
|
+
* after the existing quality enforcement block.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.isSubmitPhase = isSubmitPhase;
|
|
20
|
+
exports.isRetrospectivePhase = isRetrospectivePhase;
|
|
21
|
+
exports.isDelegationGraphPhase = isDelegationGraphPhase;
|
|
22
|
+
exports.validateReviewHandoff = validateReviewHandoff;
|
|
23
|
+
exports.validateNextJobRecommendations = validateNextJobRecommendations;
|
|
24
|
+
exports.validateDelegationLedger = validateDelegationLedger;
|
|
25
|
+
exports.validateHandoffContracts = validateHandoffContracts;
|
|
26
|
+
exports.buildHandoffRejectionMessage = buildHandoffRejectionMessage;
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Phase detection
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Returns true when a seekMentoring call is a submit-phase handoff requiring
|
|
32
|
+
* a reviewHandoff evidence field. The canonical signal is status === "awaiting_mentor"
|
|
33
|
+
* — set only by submit-phase jobs requesting a review gate — rather than a
|
|
34
|
+
* hardcoded phase name list, which is fragile for personalized jobs.
|
|
35
|
+
*/
|
|
36
|
+
function isSubmitPhase(currentPhase, status) {
|
|
37
|
+
return status === 'awaiting_mentor';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when a seekMentoring call is a retrospective completion.
|
|
41
|
+
* Retrospective jobs emit nextJobRecommendations at this boundary.
|
|
42
|
+
*/
|
|
43
|
+
function isRetrospectivePhase(currentPhase, status) {
|
|
44
|
+
return currentPhase === 'retrospective' && status === 'complete';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns true when a seekMentoring call is the delegation graph phase.
|
|
48
|
+
* This matches the existing Hub test fixture for the delegation board.
|
|
49
|
+
*/
|
|
50
|
+
function isDelegationGraphPhase(currentPhase) {
|
|
51
|
+
return currentPhase === 'create-delegation-graph';
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Individual validators
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* Validates evidence.reviewHandoff. Returns null if valid, or an array of
|
|
58
|
+
* human-readable error strings describing what is wrong.
|
|
59
|
+
*
|
|
60
|
+
* Required minimum shape:
|
|
61
|
+
* {
|
|
62
|
+
* reviewRequired: boolean,
|
|
63
|
+
* reviewTarget: object | null,
|
|
64
|
+
* artifacts: array,
|
|
65
|
+
* }
|
|
66
|
+
*/
|
|
67
|
+
function validateReviewHandoff(value) {
|
|
68
|
+
if (value === undefined || value === null) {
|
|
69
|
+
return ['evidence.reviewHandoff is missing'];
|
|
70
|
+
}
|
|
71
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
72
|
+
return ['evidence.reviewHandoff must be an object'];
|
|
73
|
+
}
|
|
74
|
+
const obj = value;
|
|
75
|
+
const errors = [];
|
|
76
|
+
if (typeof obj.reviewRequired !== 'boolean') {
|
|
77
|
+
errors.push(`evidence.reviewHandoff.reviewRequired must be a boolean (got ${obj.reviewRequired === undefined ? 'missing' : typeof obj.reviewRequired})`);
|
|
78
|
+
}
|
|
79
|
+
if (obj.reviewRequired === true) {
|
|
80
|
+
if (!Array.isArray(obj.reviewActions) || obj.reviewActions.length === 0) {
|
|
81
|
+
errors.push('evidence.reviewHandoff.reviewActions must be a non-empty array when reviewRequired is true');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const actions = obj.reviewActions;
|
|
85
|
+
const hasApprove = actions.some((a) => a.kind === 'approve');
|
|
86
|
+
const hasRequestChanges = actions.some((a) => a.kind === 'request_changes');
|
|
87
|
+
if (!hasApprove) {
|
|
88
|
+
errors.push('evidence.reviewHandoff.reviewActions must include an entry with kind: "approve"');
|
|
89
|
+
}
|
|
90
|
+
if (!hasRequestChanges) {
|
|
91
|
+
errors.push('evidence.reviewHandoff.reviewActions must include an entry with kind: "request_changes"');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return errors.length > 0 ? errors : null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validates evidence.nextJobRecommendations. Returns null if valid, or an
|
|
99
|
+
* array of error strings.
|
|
100
|
+
*
|
|
101
|
+
* Required minimum shape: an array (may be empty) of entries, each with:
|
|
102
|
+
* { jobId: string, label: string, reason?: string, contextSummary?: string }
|
|
103
|
+
*/
|
|
104
|
+
function validateNextJobRecommendations(value) {
|
|
105
|
+
if (value === undefined || value === null) {
|
|
106
|
+
return ['evidence.nextJobRecommendations is missing'];
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(value)) {
|
|
109
|
+
return ['evidence.nextJobRecommendations must be an array'];
|
|
110
|
+
}
|
|
111
|
+
const errors = [];
|
|
112
|
+
value.forEach((entry, i) => {
|
|
113
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
114
|
+
errors.push(`evidence.nextJobRecommendations[${i}] must be an object`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const rec = entry;
|
|
118
|
+
if (typeof rec.jobId !== 'string' || !rec.jobId.trim()) {
|
|
119
|
+
errors.push(`evidence.nextJobRecommendations[${i}].jobId must be a non-empty string`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof rec.label !== 'string' || !rec.label.trim()) {
|
|
122
|
+
errors.push(`evidence.nextJobRecommendations[${i}].label must be a non-empty string`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return errors.length > 0 ? errors : null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Validates evidence.delegationLedger. Returns null if valid, or an array
|
|
129
|
+
* of error strings.
|
|
130
|
+
*
|
|
131
|
+
* Required minimum shape:
|
|
132
|
+
* {
|
|
133
|
+
* delegationRequired: boolean,
|
|
134
|
+
* tasks: array,
|
|
135
|
+
* }
|
|
136
|
+
*/
|
|
137
|
+
function validateDelegationLedger(value) {
|
|
138
|
+
if (value === undefined || value === null) {
|
|
139
|
+
return ['evidence.delegationLedger is missing'];
|
|
140
|
+
}
|
|
141
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
142
|
+
return ['evidence.delegationLedger must be an object'];
|
|
143
|
+
}
|
|
144
|
+
const obj = value;
|
|
145
|
+
const errors = [];
|
|
146
|
+
if (typeof obj.delegationRequired !== 'boolean') {
|
|
147
|
+
errors.push(`evidence.delegationLedger.delegationRequired must be a boolean (got ${obj.delegationRequired === undefined ? 'missing' : typeof obj.delegationRequired})`);
|
|
148
|
+
}
|
|
149
|
+
if (!('tasks' in obj)) {
|
|
150
|
+
errors.push('evidence.delegationLedger.tasks is missing');
|
|
151
|
+
}
|
|
152
|
+
else if (!Array.isArray(obj.tasks)) {
|
|
153
|
+
errors.push('evidence.delegationLedger.tasks must be an array');
|
|
154
|
+
}
|
|
155
|
+
return errors.length > 0 ? errors : null;
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Orchestrator: called from the seekMentoring handler
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
/**
|
|
161
|
+
* Validates all handoff contracts that apply to the given seekMentoring call.
|
|
162
|
+
* Returns an empty array when no contract is violated; otherwise returns the
|
|
163
|
+
* error strings from whichever applicable validator(s) failed.
|
|
164
|
+
*/
|
|
165
|
+
function validateHandoffContracts(args) {
|
|
166
|
+
const phase = args.currentPhase ?? '';
|
|
167
|
+
const status = args.status ?? '';
|
|
168
|
+
const evidence = args.evidence ?? {};
|
|
169
|
+
if (isSubmitPhase(phase, status)) {
|
|
170
|
+
const errors = validateReviewHandoff(evidence.reviewHandoff);
|
|
171
|
+
if (errors)
|
|
172
|
+
return errors;
|
|
173
|
+
}
|
|
174
|
+
if (isRetrospectivePhase(phase, status)) {
|
|
175
|
+
const errors = validateNextJobRecommendations(evidence.nextJobRecommendations);
|
|
176
|
+
if (errors)
|
|
177
|
+
return errors;
|
|
178
|
+
}
|
|
179
|
+
if (isDelegationGraphPhase(phase)) {
|
|
180
|
+
const errors = validateDelegationLedger(evidence.delegationLedger);
|
|
181
|
+
if (errors)
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Rejection message builder
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
|
|
190
|
+
evidence: {
|
|
191
|
+
reviewHandoff: {
|
|
192
|
+
reviewRequired: true,
|
|
193
|
+
reviewTarget: { kind: "pull_request", url: "<PR URL>" } | { kind: "artifact_set", files: ["<path>"] } | null,
|
|
194
|
+
artifacts: [{ label: "<label>", path: "<path>", kind: "<kind>" }],
|
|
195
|
+
summary: "<optional summary>",
|
|
196
|
+
reviewActions: [
|
|
197
|
+
{ kind: "approve", label: "Approve" },
|
|
198
|
+
{ kind: "request_changes", label: "Request Changes" }
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
\`\`\``;
|
|
203
|
+
const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
|
|
204
|
+
evidence: {
|
|
205
|
+
nextJobRecommendations: [
|
|
206
|
+
{
|
|
207
|
+
jobId: "<job-slug>",
|
|
208
|
+
label: "<human-readable label>",
|
|
209
|
+
reason: "<optional: why this job is recommended>",
|
|
210
|
+
contextSummary: "<optional: one sentence context for the next agent>"
|
|
211
|
+
}
|
|
212
|
+
// 0–3 entries; an empty array [] is valid
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
\`\`\``;
|
|
216
|
+
const DELEGATION_LEDGER_SCHEMA = `\`\`\`javascript
|
|
217
|
+
evidence: {
|
|
218
|
+
delegationLedger: {
|
|
219
|
+
delegationRequired: true,
|
|
220
|
+
objective: "<optional objective string>",
|
|
221
|
+
tasks: [
|
|
222
|
+
{ jobId: "<job-slug>", personaKey: "<persona key or null>", briefing: "<optional briefing>" }
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
\`\`\``;
|
|
227
|
+
const FIELD_SCHEMAS = {
|
|
228
|
+
reviewHandoff: REVIEW_HANDOFF_SCHEMA,
|
|
229
|
+
nextJobRecommendations: NEXT_JOB_RECOMMENDATIONS_SCHEMA,
|
|
230
|
+
delegationLedger: DELEGATION_LEDGER_SCHEMA,
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Builds the rejection message returned to the agent when a handoff contract
|
|
234
|
+
* is violated. The message names the missing field and provides the required
|
|
235
|
+
* minimum schema so the agent can fix everything in one retry.
|
|
236
|
+
*/
|
|
237
|
+
function buildHandoffRejectionMessage(currentPhase, field, errors) {
|
|
238
|
+
const errorBullets = errors.map((e) => `- ${e}`).join('\n');
|
|
239
|
+
const schemaHint = FIELD_SCHEMAS[field] ?? '';
|
|
240
|
+
return [
|
|
241
|
+
`❌ **seekMentoring rejected** at phase \`${currentPhase}\`.`,
|
|
242
|
+
'',
|
|
243
|
+
`This call requires \`evidence.${field}\` to be present and valid so the Hub can render the correct management surface. The following problems were found:`,
|
|
244
|
+
'',
|
|
245
|
+
errorBullets,
|
|
246
|
+
'',
|
|
247
|
+
`Required minimum schema for \`evidence.${field}\`:`,
|
|
248
|
+
'',
|
|
249
|
+
schemaHint,
|
|
250
|
+
'',
|
|
251
|
+
`The job is **not** marked complete. Add the \`evidence.${field}\` object and resubmit this phase.`,
|
|
252
|
+
].join('\n');
|
|
253
|
+
}
|
|
@@ -63,6 +63,7 @@ const object_utils_1 = require("../core/utils/object-utils");
|
|
|
63
63
|
const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
|
|
64
64
|
const ai_mentor_1 = require("../core/ai-mentor");
|
|
65
65
|
const quality_evidence_1 = require("../core/quality-evidence");
|
|
66
|
+
const handoff_contracts_1 = require("../core/handoff-contracts");
|
|
66
67
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
67
68
|
const usage_collector_js_1 = require("./usage-collector.js");
|
|
68
69
|
const otlp_metrics_receiver_js_1 = require("./otlp-metrics-receiver.js");
|
|
@@ -2151,6 +2152,26 @@ class FraimLocalMCPServer {
|
|
|
2151
2152
|
this.log(`⚠️ Quality score emission failed: ${err?.message || err}`);
|
|
2152
2153
|
});
|
|
2153
2154
|
}
|
|
2155
|
+
// Handoff contract enforcement (Issue #916).
|
|
2156
|
+
//
|
|
2157
|
+
// The proxy is the primary enforcement point for reviewHandoff,
|
|
2158
|
+
// nextJobRecommendations, and delegationLedger evidence fields.
|
|
2159
|
+
// The Hub cannot render review bars, next-job chips, or the
|
|
2160
|
+
// delegation board when these fields are absent.
|
|
2161
|
+
const handoffErrors = (0, handoff_contracts_1.validateHandoffContracts)({
|
|
2162
|
+
jobName: args.jobName,
|
|
2163
|
+
currentPhase: args.currentPhase,
|
|
2164
|
+
status: args.status,
|
|
2165
|
+
evidence: args.evidence,
|
|
2166
|
+
});
|
|
2167
|
+
if (handoffErrors.length > 0) {
|
|
2168
|
+
const missingField = handoffErrors[0].includes('reviewHandoff') ? 'reviewHandoff'
|
|
2169
|
+
: handoffErrors[0].includes('nextJobRecommendations') ? 'nextJobRecommendations'
|
|
2170
|
+
: 'delegationLedger';
|
|
2171
|
+
this.log(`⚠️ Handoff contract enforcement rejected seekMentoring for ${args.jobName}:${args.currentPhase}: ${handoffErrors.join('; ')}`);
|
|
2172
|
+
const rejection = (0, handoff_contracts_1.buildHandoffRejectionMessage)(args.currentPhase, missingField, handoffErrors);
|
|
2173
|
+
return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
|
|
2174
|
+
}
|
|
2154
2175
|
return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, tutoringResponse.message);
|
|
2155
2176
|
}
|
|
2156
2177
|
catch (error) {
|
|
@@ -30,9 +30,21 @@ class AuthMiddleware {
|
|
|
30
30
|
// Skip auth for public health check, admin routes, website signup, sales inquiries, and self-serve access flow
|
|
31
31
|
const rawPath = req.path ?? req.originalUrl ?? req.url ?? '';
|
|
32
32
|
const p = (typeof rawPath === 'string' ? rawPath : '').split('?')[0].replace(/\/$/, '') || '';
|
|
33
|
+
const publicBaseUrl = (process.env.FRAIM_PUBLIC_BASE_URL || 'https://fraim.wellnessatwork.me').replace(/\/+$/, '');
|
|
34
|
+
const mcpResourceMetadataUrl = `${publicBaseUrl}/.well-known/oauth-protected-resource/mcp`;
|
|
35
|
+
const isMcpPath = p === '/mcp' || p.startsWith('/mcp/');
|
|
36
|
+
const setMcpAuthenticateChallenge = () => {
|
|
37
|
+
if (isMcpPath) {
|
|
38
|
+
res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${mcpResourceMetadataUrl}"`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
33
41
|
const publicPrefixes = ['/admin', '/dashboard', '/health', '/pricing', '/fraim-brain', '/api/signup', '/api/sales', '/api/request-access', '/api/installer-key', '/api/installer-download', '/api/installer-availability', '/api/payment/bypass', '/api/pricing', '/api/personas/catalog', '/auth', '/portfolio'];
|
|
34
42
|
// Analytics dashboard is public, but API routes are protected
|
|
35
43
|
const isAnalyticsPublic = p === '/analytics' || p === '/analytics/' || p.startsWith('/analytics/') && p.endsWith('.html');
|
|
44
|
+
const isOAuthMetadataPublic = p === '/.well-known/oauth-authorization-server'
|
|
45
|
+
|| p === '/.well-known/oauth-protected-resource'
|
|
46
|
+
|| p === '/.well-known/oauth-protected-resource/mcp'
|
|
47
|
+
|| p === '/.well-known/openai-apps-challenge';
|
|
36
48
|
// Homepage is public
|
|
37
49
|
if (p === '' || p === '/')
|
|
38
50
|
return next();
|
|
@@ -40,7 +52,8 @@ class AuthMiddleware {
|
|
|
40
52
|
const isPublic = publicPrefixes.some(prefix => p === prefix || p.startsWith(prefix + '/'))
|
|
41
53
|
|| publicExtensions.some(ext => p.endsWith(ext))
|
|
42
54
|
|| p.startsWith('/css/') || p.startsWith('/js/') || p.startsWith('/images/')
|
|
43
|
-
|| isAnalyticsPublic
|
|
55
|
+
|| isAnalyticsPublic
|
|
56
|
+
|| isOAuthMetadataPublic;
|
|
44
57
|
if (isPublic) {
|
|
45
58
|
return next();
|
|
46
59
|
}
|
|
@@ -102,6 +115,7 @@ class AuthMiddleware {
|
|
|
102
115
|
reason: !session ? 'not_found' : session.revoked ? 'revoked' : 'expired',
|
|
103
116
|
outcome: 'failure',
|
|
104
117
|
}).catch(() => undefined);
|
|
118
|
+
setMcpAuthenticateChallenge();
|
|
105
119
|
return res.status(401).json({
|
|
106
120
|
jsonrpc: '2.0',
|
|
107
121
|
error: { code: -32001, message: 'Unauthorized: Session expired or revoked [AUTH-SESSION-INVALID]' },
|
|
@@ -111,6 +125,7 @@ class AuthMiddleware {
|
|
|
111
125
|
const apiKeyData = await this.dbService.getApiKeyByUserId(session.userId, false);
|
|
112
126
|
if (!apiKeyData) {
|
|
113
127
|
console.error(`[FRAIM AUTH] Session valid but no API key for user ${session.userId}`);
|
|
128
|
+
setMcpAuthenticateChallenge();
|
|
114
129
|
return res.status(401).json({
|
|
115
130
|
jsonrpc: '2.0',
|
|
116
131
|
error: { code: -32001, message: 'Unauthorized: No identity for session [AUTH-NO-IDENTITY]' },
|
|
@@ -146,6 +161,7 @@ class AuthMiddleware {
|
|
|
146
161
|
outcome: 'failure',
|
|
147
162
|
reason: `session_user=${session.userId}`,
|
|
148
163
|
}).catch(() => undefined);
|
|
164
|
+
setMcpAuthenticateChallenge();
|
|
149
165
|
return res.status(401).json({
|
|
150
166
|
jsonrpc: '2.0',
|
|
151
167
|
error: { code: -32001, message: 'Unauthorized: Mixed identity [AUTH-MIXED-IDENTITY]' },
|
|
@@ -169,6 +185,7 @@ class AuthMiddleware {
|
|
|
169
185
|
return res.status(404).send('<h1>404</h1><p>Page not found.</p>');
|
|
170
186
|
}
|
|
171
187
|
console.error(`[FRAIM AUTH] Missing API key for ${req.method} ${req.path}`);
|
|
188
|
+
setMcpAuthenticateChallenge();
|
|
172
189
|
return res.status(401).json({
|
|
173
190
|
jsonrpc: '2.0',
|
|
174
191
|
error: { code: -32001, message: 'Unauthorized: Missing API key [AUTH-MISSING]' },
|
|
@@ -194,6 +211,7 @@ class AuthMiddleware {
|
|
|
194
211
|
const apiKeyData = await this.dbService.verifyApiKey(apiKey);
|
|
195
212
|
if (!apiKeyData) {
|
|
196
213
|
console.error(`❌ FRAIM AUTH: Invalid API key: ${apiKey}`);
|
|
214
|
+
setMcpAuthenticateChallenge();
|
|
197
215
|
return res.status(401).json({
|
|
198
216
|
jsonrpc: '2.0',
|
|
199
217
|
error: { code: -32001, message: 'Unauthorized: Invalid x-api-key [AUTH-INVALID]' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.228",
|
|
4
4
|
"description": "FRAIM core CLI and MCP package.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@octokit/rest": "^22.0.1",
|
|
41
|
-
"adm-zip": "^0.
|
|
41
|
+
"adm-zip": "^0.6.0",
|
|
42
42
|
"axios": "^1.7.0",
|
|
43
43
|
"chalk": "4.1.2",
|
|
44
44
|
"commander": "^14.0.2",
|