@goodandready/dsh-cron 0.3.22 → 0.3.23
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/lib/chat-start.js +13 -2
- package/lib/index.js +14 -1
- package/lib/models-handler.js +14 -6
- package/lib/routes.js +24 -0
- package/lib/runner.js +46 -7
- package/lib/task-create.js +3 -1
- package/lib/task-schema.js +175 -0
- package/package.json +11 -2
package/lib/chat-start.js
CHANGED
|
@@ -18,7 +18,12 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
let agents = null;
|
|
22
|
+
try {
|
|
23
|
+
agents = (ctx && typeof ctx.get === 'function' ? ctx.get('agents') : null) || (ctx && ctx.agents);
|
|
24
|
+
} catch {
|
|
25
|
+
agents = null;
|
|
26
|
+
}
|
|
22
27
|
if (!agents || typeof agents.create !== 'function') {
|
|
23
28
|
sendJson(res, 500, { ok: false, error: 'The agents service is unavailable in DSH' });
|
|
24
29
|
return;
|
|
@@ -45,7 +50,13 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
|
|
|
45
50
|
(ctx.logger || console).debug?.('[dsh-cron] @deepseek-ai/dsh-session SessionId unavailable, using string fallback:', err?.message || err);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
let defaultSel = null;
|
|
54
|
+
try {
|
|
55
|
+
const adm = (ctx && typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) || (ctx && ctx.agentDefaultModel);
|
|
56
|
+
defaultSel = adm?.currentSelection?.();
|
|
57
|
+
} catch {
|
|
58
|
+
defaultSel = null;
|
|
59
|
+
}
|
|
49
60
|
const provider = body.provider || defaultSel?.provider;
|
|
50
61
|
const model = body.model || defaultSel?.model;
|
|
51
62
|
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TaskSchema,
|
|
3
|
+
validateTaskRecord,
|
|
4
|
+
safeValidateTaskRecord,
|
|
5
|
+
validateTaskPatch,
|
|
6
|
+
} from './task-schema.js';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
TaskSchema,
|
|
10
|
+
validateTaskRecord,
|
|
11
|
+
safeValidateTaskRecord,
|
|
12
|
+
validateTaskPatch,
|
|
13
|
+
};
|
|
1
14
|
import fs from 'node:fs';
|
|
2
15
|
import path from 'node:path';
|
|
3
16
|
import { bestEffort } from './best-effort.js';
|
|
@@ -95,7 +108,7 @@ export {
|
|
|
95
108
|
};
|
|
96
109
|
|
|
97
110
|
export const name = '@goodandready/dsh-cron';
|
|
98
|
-
export const inject = ['tools', 'webServer'
|
|
111
|
+
export const inject = ['tools', 'webServer'];
|
|
99
112
|
|
|
100
113
|
import { executeCreateTask } from './task-create.js';
|
|
101
114
|
import {
|
package/lib/models-handler.js
CHANGED
|
@@ -3,11 +3,18 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
|
3
3
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
4
4
|
return;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
let current = null;
|
|
7
|
+
try {
|
|
8
|
+
const adm = (ctx && typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) || (ctx && ctx.agentDefaultModel);
|
|
9
|
+
current = adm?.currentSelection?.() ?? null;
|
|
10
|
+
} catch {
|
|
11
|
+
current = null;
|
|
12
|
+
}
|
|
7
13
|
let providers = [];
|
|
8
14
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const llm = (ctx && typeof ctx.get === 'function' ? ctx.get('llm') : null) || (ctx && ctx.llm);
|
|
16
|
+
if (llm && typeof llm.listProviders === 'function') {
|
|
17
|
+
providers = (llm.listProviders() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id }));
|
|
11
18
|
}
|
|
12
19
|
} catch {
|
|
13
20
|
providers = [];
|
|
@@ -22,8 +29,9 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
|
22
29
|
|
|
23
30
|
try {
|
|
24
31
|
let rows = [];
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const llm = (ctx && typeof ctx.get === 'function' ? ctx.get('llm') : null) || (ctx && ctx.llm);
|
|
33
|
+
if (llm && typeof llm.listModels === 'function') {
|
|
34
|
+
rows = await llm.listModels(wanted);
|
|
27
35
|
}
|
|
28
36
|
sendJson(res, 200, {
|
|
29
37
|
current,
|
|
@@ -33,4 +41,4 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
|
33
41
|
} catch {
|
|
34
42
|
sendJson(res, 200, { current, providers, models: [] });
|
|
35
43
|
}
|
|
36
|
-
}
|
|
44
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -22,6 +22,30 @@ const LOCALES_JSON = readFileSync(new URL('./locales.json', import.meta.url), 'u
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
25
|
+
// Health probe route (#384)
|
|
26
|
+
ctx.effect(() => ctx.webServer.register({
|
|
27
|
+
kind: 'exact',
|
|
28
|
+
path: '/dsh-cron/health',
|
|
29
|
+
handler: (req, res) => {
|
|
30
|
+
if (req.method !== 'GET') {
|
|
31
|
+
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
32
|
+
res.end('GET only');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
res.writeHead(200, {
|
|
36
|
+
'content-type': 'application/json; charset=utf-8',
|
|
37
|
+
'cache-control': 'no-store'
|
|
38
|
+
});
|
|
39
|
+
const tasks = typeof store?.list === 'function' ? store.list().length : 0;
|
|
40
|
+
res.end(JSON.stringify({
|
|
41
|
+
status: 'ok',
|
|
42
|
+
plugin: '@goodandready/dsh-cron',
|
|
43
|
+
tasks,
|
|
44
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}), 'dsh-cron: /health');
|
|
48
|
+
|
|
25
49
|
// Client locales route (#374)
|
|
26
50
|
ctx.effect(() => ctx.webServer.register({
|
|
27
51
|
kind: 'exact',
|
package/lib/runner.js
CHANGED
|
@@ -79,6 +79,40 @@ export class SessionRunner {
|
|
|
79
79
|
this.store = store;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
_getAgentsService() {
|
|
83
|
+
if (!this.ctx) return null;
|
|
84
|
+
try {
|
|
85
|
+
if (typeof this.ctx.get === 'function') {
|
|
86
|
+
const svc = this.ctx.get('agents');
|
|
87
|
+
if (svc) return svc;
|
|
88
|
+
}
|
|
89
|
+
} catch (_err) {
|
|
90
|
+
// service unavailable via ctx.get
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return this.ctx.agents || null;
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_getSessionService() {
|
|
100
|
+
if (!this.ctx) return null;
|
|
101
|
+
try {
|
|
102
|
+
if (typeof this.ctx.get === 'function') {
|
|
103
|
+
const svc = this.ctx.get('session');
|
|
104
|
+
if (svc) return svc;
|
|
105
|
+
}
|
|
106
|
+
} catch (_err) {
|
|
107
|
+
// service unavailable via ctx.get
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return this.ctx.session || null;
|
|
111
|
+
} catch (_err) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
async execute(task, options = {}) {
|
|
83
117
|
const timeoutSeconds = Number(task.timeoutSeconds) > 0 ? Number(task.timeoutSeconds) : 1800;
|
|
84
118
|
const timeoutMs = timeoutSeconds * 1000;
|
|
@@ -234,12 +268,14 @@ export class SessionRunner {
|
|
|
234
268
|
}
|
|
235
269
|
|
|
236
270
|
// 2. Agent session, when the harness exposes one
|
|
237
|
-
|
|
271
|
+
const agentsSvc = this._getAgentsService();
|
|
272
|
+
if (agentsSvc && typeof agentsSvc.create === 'function') {
|
|
238
273
|
return this._runAgentSession(effectiveTask, runtime, signal, options);
|
|
239
274
|
}
|
|
240
275
|
|
|
241
276
|
// 3. Legacy session service
|
|
242
|
-
|
|
277
|
+
const sessionSvc = this._getSessionService();
|
|
278
|
+
if (sessionSvc && typeof sessionSvc.create === 'function') {
|
|
243
279
|
return this._runLegacySession(effectiveTask, options);
|
|
244
280
|
}
|
|
245
281
|
|
|
@@ -270,12 +306,14 @@ export class SessionRunner {
|
|
|
270
306
|
}
|
|
271
307
|
|
|
272
308
|
// 2. Agent session, when the harness exposes one
|
|
273
|
-
|
|
309
|
+
const agentsSvc2 = this._getAgentsService();
|
|
310
|
+
if (agentsSvc2 && typeof agentsSvc2.create === 'function') {
|
|
274
311
|
return this._runAgentSession(task, runtime, signal, options);
|
|
275
312
|
}
|
|
276
313
|
|
|
277
314
|
// 3. Legacy session service
|
|
278
|
-
|
|
315
|
+
const sessionSvc2 = this._getSessionService();
|
|
316
|
+
if (sessionSvc2 && typeof sessionSvc2.create === 'function') {
|
|
279
317
|
return this._runLegacySession(task, options);
|
|
280
318
|
}
|
|
281
319
|
|
|
@@ -484,9 +522,10 @@ export class SessionRunner {
|
|
|
484
522
|
|
|
485
523
|
// If targetSessionId is set, attempt to resume the existing session (#143, #346, GH #9)
|
|
486
524
|
let resumeFailure = null;
|
|
487
|
-
|
|
525
|
+
const agents = this._getAgentsService();
|
|
526
|
+
if (prep.targetSessionId && agents && typeof agents.resume === 'function') {
|
|
488
527
|
try {
|
|
489
|
-
handle = await
|
|
528
|
+
handle = await agents.resume({
|
|
490
529
|
sessionId: prep.targetSessionId,
|
|
491
530
|
resumeSessionId: prep.targetSessionId,
|
|
492
531
|
meta: {
|
|
@@ -508,7 +547,7 @@ export class SessionRunner {
|
|
|
508
547
|
// Create new session if not resumed
|
|
509
548
|
if (!handle) {
|
|
510
549
|
try {
|
|
511
|
-
handle = await
|
|
550
|
+
handle = await agents.create({
|
|
512
551
|
sessionId: prep.mintSessionId(),
|
|
513
552
|
meta: {
|
|
514
553
|
cwd: prep.cwd,
|
package/lib/task-create.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateTaskRecord } from './task-schema.js';
|
|
1
2
|
import { sanitizeSwarmConfig } from './swarm.js';
|
|
2
3
|
import { sanitizeBranchRules } from './branching.js';
|
|
3
4
|
import { sanitizePromptHistory } from './prompt-evolver.js';
|
|
@@ -132,7 +133,8 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
132
133
|
throw new Error(dagCheck.error);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
const
|
|
136
|
+
const validTask = validateTaskRecord(taskData);
|
|
137
|
+
const task = store.set(validTask);
|
|
136
138
|
scheduler.scheduleTask(task);
|
|
137
139
|
return {
|
|
138
140
|
success: true,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { normalizeTaskType } from './runtimes.js';
|
|
3
|
+
import { validateTaskType, PATCHABLE_TASK_FIELDS } from './task-transfer.js';
|
|
4
|
+
import { CHANNEL_IDS } from './channels.js';
|
|
5
|
+
|
|
6
|
+
export const TaskSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
title: z.string().required(),
|
|
9
|
+
schedule: z.string().required(),
|
|
10
|
+
scheduleText: z.string(),
|
|
11
|
+
type: z.string().default('llm'),
|
|
12
|
+
prompt: z.string().default(''),
|
|
13
|
+
delivery: z.string().default('isolated'),
|
|
14
|
+
status: z.string().default('active'),
|
|
15
|
+
provider: z.string(),
|
|
16
|
+
model: z.string(),
|
|
17
|
+
fallbackProvider: z.string(),
|
|
18
|
+
fallbackModel: z.string(),
|
|
19
|
+
silentRule: z.string().default(''),
|
|
20
|
+
inspectOnFailure: z.boolean().default(false),
|
|
21
|
+
agentPreset: z.string().default(''),
|
|
22
|
+
targetSessionId: z.string().default(''),
|
|
23
|
+
targetSessionReset: z.string().default('never'),
|
|
24
|
+
sessionTitle: z.string().default(''),
|
|
25
|
+
timezone: z.string().default(''),
|
|
26
|
+
misfirePolicy: z.string().default('skip'),
|
|
27
|
+
maxRetries: z.number().default(0),
|
|
28
|
+
retryBackoffMs: z.number().default(30000),
|
|
29
|
+
permissionPreset: z.string().default('default'),
|
|
30
|
+
captureGitDiff: z.boolean(),
|
|
31
|
+
kanbanMode: z.string().default('none'),
|
|
32
|
+
channels: z.array(z.string()).default([]),
|
|
33
|
+
template: z.string().default(''),
|
|
34
|
+
env: z.any(),
|
|
35
|
+
cwd: z.string().default(''),
|
|
36
|
+
workspaceId: z.string().default(''),
|
|
37
|
+
worktree: z.boolean().default(false),
|
|
38
|
+
keepWorktree: z.boolean().default(false),
|
|
39
|
+
httpMethod: z.string().default('GET'),
|
|
40
|
+
httpUrl: z.string().default(''),
|
|
41
|
+
httpHeaders: z.any(),
|
|
42
|
+
httpBody: z.string().default(''),
|
|
43
|
+
sshProfileId: z.string().default(''),
|
|
44
|
+
sshTarget: z.string().default(''),
|
|
45
|
+
sshPort: z.number().default(0),
|
|
46
|
+
sshKeyPath: z.string().default(''),
|
|
47
|
+
dockerImage: z.string().default(''),
|
|
48
|
+
pythonPath: z.string().default(''),
|
|
49
|
+
nodePath: z.string().default(''),
|
|
50
|
+
skillName: z.string().default(''),
|
|
51
|
+
workflowName: z.string().default(''),
|
|
52
|
+
webhookEnabled: z.boolean().default(false),
|
|
53
|
+
webhookToken: z.string(),
|
|
54
|
+
webhookSecret: z.string(),
|
|
55
|
+
webhookDebounceMs: z.number(),
|
|
56
|
+
webhookTrigger: z.any(),
|
|
57
|
+
onSuccess: z.string().default(''),
|
|
58
|
+
onFailure: z.string().default(''),
|
|
59
|
+
heartbeatIntervalSeconds: z.number().default(0),
|
|
60
|
+
gracePeriodSeconds: z.number().default(300),
|
|
61
|
+
preflightType: z.string().default('none'),
|
|
62
|
+
preflightTarget: z.string().default(''),
|
|
63
|
+
preflightCommand: z.string(),
|
|
64
|
+
preflightScript: z.string(),
|
|
65
|
+
concurrencyGroup: z.string().default('default'),
|
|
66
|
+
priority: z.number().default(5),
|
|
67
|
+
selfHealingCommand: z.string().default(''),
|
|
68
|
+
autoDiagnose: z.boolean().default(false),
|
|
69
|
+
notifyTelegram: z.boolean(),
|
|
70
|
+
onlyOnFailure: z.boolean(),
|
|
71
|
+
timeoutSeconds: z.number().default(1800),
|
|
72
|
+
overlapPolicy: z.string().default('skip'),
|
|
73
|
+
oneShot: z.boolean().default(false),
|
|
74
|
+
targetTimestamp: z.number(),
|
|
75
|
+
nextRunAt: z.number(),
|
|
76
|
+
costLimitUsd: z.number(),
|
|
77
|
+
dailyCostLimitUsd: z.number(),
|
|
78
|
+
tokenLimit: z.number(),
|
|
79
|
+
pausedReason: z.string(),
|
|
80
|
+
validators: z.any(),
|
|
81
|
+
requireApproval: z.boolean().default(false),
|
|
82
|
+
fileWatch: z.any(),
|
|
83
|
+
dependsOn: z.array(z.string()).default([]),
|
|
84
|
+
joinPolicy: z.string().default('all_success'),
|
|
85
|
+
adaptive: z.any(),
|
|
86
|
+
adaptiveState: z.any(),
|
|
87
|
+
promptVersion: z.number(),
|
|
88
|
+
promptHistory: z.any(),
|
|
89
|
+
branchRules: z.any(),
|
|
90
|
+
elseTask: z.string(),
|
|
91
|
+
autoHeal: z.boolean(),
|
|
92
|
+
selfHealing: z.any(),
|
|
93
|
+
revisions: z.any(),
|
|
94
|
+
previousPrompt: z.string(),
|
|
95
|
+
previousScript: z.string(),
|
|
96
|
+
swarm: z.any(),
|
|
97
|
+
gitDiff: z.any(),
|
|
98
|
+
schemaVersion: z.number().default(1),
|
|
99
|
+
createdAt: z.number(),
|
|
100
|
+
updatedAt: z.number(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export function validateTaskRecord(data) {
|
|
104
|
+
if (!data || typeof data !== 'object') {
|
|
105
|
+
throw new TypeError('Task record must be an object');
|
|
106
|
+
}
|
|
107
|
+
if (data.title !== undefined && typeof data.title !== 'string') {
|
|
108
|
+
throw new Error('Task schema validation failed: title must be a string');
|
|
109
|
+
}
|
|
110
|
+
const title = String(data.title || '').trim();
|
|
111
|
+
if (!title) {
|
|
112
|
+
throw new Error('Task title cannot be empty');
|
|
113
|
+
}
|
|
114
|
+
if (data.schedule !== undefined && typeof data.schedule !== 'string') {
|
|
115
|
+
throw new Error('Task schema validation failed: schedule must be a string');
|
|
116
|
+
}
|
|
117
|
+
const schedule = String(data.schedule || '').trim();
|
|
118
|
+
if (!schedule) {
|
|
119
|
+
throw new Error('Task schedule cannot be empty');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cleanType = normalizeTaskType(data.type);
|
|
123
|
+
const typeError = validateTaskType(cleanType, data);
|
|
124
|
+
if (typeError) {
|
|
125
|
+
throw new Error(typeError);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let validated;
|
|
129
|
+
try {
|
|
130
|
+
validated = TaskSchema({
|
|
131
|
+
...data,
|
|
132
|
+
title,
|
|
133
|
+
schedule,
|
|
134
|
+
type: cleanType,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new Error(`Task schema validation failed: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Filter channels to known channel ids if present
|
|
141
|
+
if (Array.isArray(validated.channels)) {
|
|
142
|
+
validated.channels = validated.channels.filter((c) => CHANNEL_IDS.includes(c));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Strip undefined keys
|
|
146
|
+
for (const k of Object.keys(validated)) {
|
|
147
|
+
if (validated[k] === undefined) {
|
|
148
|
+
delete validated[k];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return validated;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function safeValidateTaskRecord(data) {
|
|
156
|
+
try {
|
|
157
|
+
const task = validateTaskRecord(data);
|
|
158
|
+
return { ok: true, task };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { ok: false, error: err.message };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function validateTaskPatch(patch) {
|
|
165
|
+
if (!patch || typeof patch !== 'object') {
|
|
166
|
+
return { ok: false, error: 'Task patch must be an object' };
|
|
167
|
+
}
|
|
168
|
+
const clean = {};
|
|
169
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
170
|
+
if (PATCHABLE_TASK_FIELDS.includes(key)) {
|
|
171
|
+
clean[key] = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, patch: clean };
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"test": "node --test test/*.test.mjs && node scripts/check-pack-size.mjs",
|
|
23
23
|
"build:client": "node scripts/build-client.mjs",
|
|
24
24
|
"pretest": "node scripts/build-client.mjs",
|
|
25
|
-
"preflight": "node scripts/ci-preflight.mjs"
|
|
25
|
+
"preflight": "node scripts/ci-preflight.mjs",
|
|
26
|
+
"test:engine": "node scripts/test-engines.mjs"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"dsh",
|
|
@@ -51,6 +52,14 @@
|
|
|
51
52
|
"@deepseek-ai/dsh-client-locale",
|
|
52
53
|
"@deepseek-ai/dsh-client-ui-slots"
|
|
53
54
|
]
|
|
55
|
+
},
|
|
56
|
+
"compatibility": {
|
|
57
|
+
"dshReleases": {
|
|
58
|
+
"0.2.1-rc.2": "compatible",
|
|
59
|
+
"0.2.1-rc.3": "compatible",
|
|
60
|
+
"0.2.1-rc.4": "compatible"
|
|
61
|
+
},
|
|
62
|
+
"dsh": ">=0.2.1-rc.2 <0.2.2-0"
|
|
54
63
|
}
|
|
55
64
|
},
|
|
56
65
|
"dependencies": {
|