@dezycro-ai/agent-plugin 2.1.0 → 2.1.2
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/dist/hooks/lib/command-bus.d.ts +14 -1
- package/dist/hooks/lib/command-bus.js +67 -17
- package/dist/hooks/lib/router.js +48 -0
- package/dist/hooks/lib/state.js +1 -0
- package/dist/hooks/types.d.ts +15 -0
- package/package.json +1 -1
- package/setup.js +23 -5
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* has elapsed since the last successful poll.
|
|
9
9
|
* * On 2xx: advance cursor + lastPollTs, dedup by `eventId` (recent ring, cap 200),
|
|
10
10
|
* return new events for surfacing.
|
|
11
|
-
* * On non-2xx / network failure: cursor stays put, consecutiveFailures
|
|
11
|
+
* * On non-2xx / network failure: cursor stays put, consecutiveFailures++, and
|
|
12
|
+
* `lastAttemptTs` advances so the floor throttles repeated failures. Once
|
|
13
|
+
* failures cross `circuitBreakerThreshold` the floor grows exponentially
|
|
14
|
+
* (capped at `maxBackoffSeconds`), so an unreachable endpoint is not polled on
|
|
15
|
+
* every hook fire. A single success resets the counter and the interval.
|
|
12
16
|
*
|
|
13
17
|
* Auth: a bearer token from `.dezycro/companion-token.local.json` (`{"token": "dzy_…"}`)
|
|
14
18
|
* is sent on every request. Separate from the MCP server token (the user maintains both).
|
|
@@ -27,7 +31,15 @@ interface ResolvedConfig {
|
|
|
27
31
|
hooks: ReadonlySet<CommandBusHook>;
|
|
28
32
|
minPollIntervalSeconds: number;
|
|
29
33
|
reactionMode: CommandBusReactionMode;
|
|
34
|
+
circuitBreakerThreshold: number;
|
|
35
|
+
maxBackoffSeconds: number;
|
|
30
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Effective poll interval given the number of consecutive failures so far.
|
|
39
|
+
* Returns the base interval until `threshold` failures, then doubles per extra
|
|
40
|
+
* failure, capped at `maxSeconds`. Never throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function backoffSeconds(baseSeconds: number, consecutiveFailures: number, threshold: number, maxSeconds: number): number;
|
|
31
43
|
/**
|
|
32
44
|
* Entry point called from each hook handler. Returns the events that should be surfaced
|
|
33
45
|
* to the agent on this hook fire, plus a state patch the caller must apply.
|
|
@@ -49,5 +61,6 @@ export declare const _internals: {
|
|
|
49
61
|
renderEvent: typeof renderEvent;
|
|
50
62
|
reactionGuidance: typeof reactionGuidance;
|
|
51
63
|
trimRecent: typeof trimRecent;
|
|
64
|
+
backoffSeconds: typeof backoffSeconds;
|
|
52
65
|
};
|
|
53
66
|
export {};
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* has elapsed since the last successful poll.
|
|
9
9
|
* * On 2xx: advance cursor + lastPollTs, dedup by `eventId` (recent ring, cap 200),
|
|
10
10
|
* return new events for surfacing.
|
|
11
|
-
* * On non-2xx / network failure: cursor stays put, consecutiveFailures
|
|
11
|
+
* * On non-2xx / network failure: cursor stays put, consecutiveFailures++, and
|
|
12
|
+
* `lastAttemptTs` advances so the floor throttles repeated failures. Once
|
|
13
|
+
* failures cross `circuitBreakerThreshold` the floor grows exponentially
|
|
14
|
+
* (capped at `maxBackoffSeconds`), so an unreachable endpoint is not polled on
|
|
15
|
+
* every hook fire. A single success resets the counter and the interval.
|
|
12
16
|
*
|
|
13
17
|
* Auth: a bearer token from `.dezycro/companion-token.local.json` (`{"token": "dzy_…"}`)
|
|
14
18
|
* is sent on every request. Separate from the MCP server token (the user maintains both).
|
|
@@ -52,9 +56,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
52
56
|
})();
|
|
53
57
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
58
|
exports._internals = void 0;
|
|
59
|
+
exports.backoffSeconds = backoffSeconds;
|
|
55
60
|
exports.pollIfDue = pollIfDue;
|
|
56
61
|
exports.renderForAgent = renderForAgent;
|
|
57
62
|
const fs = __importStar(require("fs"));
|
|
63
|
+
const os = __importStar(require("os"));
|
|
58
64
|
const path = __importStar(require("path"));
|
|
59
65
|
const logging = __importStar(require("./logging"));
|
|
60
66
|
const DEFAULT_HOOKS = [
|
|
@@ -67,6 +73,24 @@ const DEFAULT_MIN_POLL_INTERVAL_SECONDS = 60;
|
|
|
67
73
|
const DEFAULT_REACTION_MODE = 'HALT_AND_REPLAN';
|
|
68
74
|
const RECENT_EVENT_IDS_CAP = 200;
|
|
69
75
|
const POLL_LIMIT = 100;
|
|
76
|
+
// After this many consecutive failures the poll interval starts doubling per
|
|
77
|
+
// extra failure (capped at maxBackoffSeconds) so an unreachable inbox endpoint
|
|
78
|
+
// — e.g. a default `local` env that isn't running — stops being polled on every
|
|
79
|
+
// hook fire. Self-heals: a single success resets consecutiveFailures to 0.
|
|
80
|
+
const DEFAULT_CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
81
|
+
const DEFAULT_MAX_BACKOFF_SECONDS = 1800;
|
|
82
|
+
/**
|
|
83
|
+
* Effective poll interval given the number of consecutive failures so far.
|
|
84
|
+
* Returns the base interval until `threshold` failures, then doubles per extra
|
|
85
|
+
* failure, capped at `maxSeconds`. Never throws.
|
|
86
|
+
*/
|
|
87
|
+
function backoffSeconds(baseSeconds, consecutiveFailures, threshold, maxSeconds) {
|
|
88
|
+
if (consecutiveFailures < threshold)
|
|
89
|
+
return baseSeconds;
|
|
90
|
+
const extra = consecutiveFailures - threshold + 1;
|
|
91
|
+
const scaled = baseSeconds * Math.pow(2, extra);
|
|
92
|
+
return Math.min(scaled, maxSeconds);
|
|
93
|
+
}
|
|
70
94
|
/**
|
|
71
95
|
* Entry point called from each hook handler. Returns the events that should be surfaced
|
|
72
96
|
* to the agent on this hook fire, plus a state patch the caller must apply.
|
|
@@ -84,11 +108,17 @@ async function pollIfDue(repoRoot, hookEvent, config, state) {
|
|
|
84
108
|
// SessionStart always polls regardless of the configured hook set — bootstrap-on-start
|
|
85
109
|
// is non-negotiable for cursor recovery after compaction.
|
|
86
110
|
const cbState = state.commandBus;
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
// Throttle from the last ATTEMPT (success or failure), not just the last
|
|
112
|
+
// success — otherwise a never-succeeding endpoint (lastPollTs stays null) is
|
|
113
|
+
// polled on every hook fire. The floor grows via exponential backoff once
|
|
114
|
+
// consecutive failures cross the circuit-breaker threshold.
|
|
115
|
+
const lastAttempt = cbState.lastAttemptTs ?? cbState.lastPollTs;
|
|
116
|
+
if (hookEvent !== 'sessionStart' && lastAttempt) {
|
|
117
|
+
const lastMs = Date.parse(lastAttempt);
|
|
89
118
|
if (!Number.isNaN(lastMs)) {
|
|
119
|
+
const floorSeconds = backoffSeconds(resolved.minPollIntervalSeconds, cbState.consecutiveFailures ?? 0, resolved.circuitBreakerThreshold, resolved.maxBackoffSeconds);
|
|
90
120
|
const ageSeconds = (Date.now() - lastMs) / 1000;
|
|
91
|
-
if (ageSeconds <
|
|
121
|
+
if (ageSeconds < floorSeconds)
|
|
92
122
|
return empty;
|
|
93
123
|
}
|
|
94
124
|
}
|
|
@@ -104,6 +134,7 @@ async function pollIfDue(repoRoot, hookEvent, config, state) {
|
|
|
104
134
|
return empty;
|
|
105
135
|
}
|
|
106
136
|
const since = hookEvent === 'sessionStart' ? null : cbState.cursor;
|
|
137
|
+
const attemptTs = new Date().toISOString();
|
|
107
138
|
try {
|
|
108
139
|
const fetched = await fetchInbox(apiUrl, feature, token, since);
|
|
109
140
|
const seen = new Set(cbState.recentEventIds ?? []);
|
|
@@ -113,6 +144,7 @@ async function pollIfDue(repoRoot, hookEvent, config, state) {
|
|
|
113
144
|
commandBus: {
|
|
114
145
|
cursor: fetched.nextCursor || cbState.cursor,
|
|
115
146
|
lastPollTs: fetched.serverTs,
|
|
147
|
+
lastAttemptTs: attemptTs,
|
|
116
148
|
consecutiveFailures: 0,
|
|
117
149
|
recentEventIds: newRing,
|
|
118
150
|
},
|
|
@@ -127,16 +159,21 @@ async function pollIfDue(repoRoot, hookEvent, config, state) {
|
|
|
127
159
|
return { events: fresh, statePatch: patch, reactionMode };
|
|
128
160
|
}
|
|
129
161
|
catch (e) {
|
|
162
|
+
const consecutiveFailures = (cbState.consecutiveFailures ?? 0) + 1;
|
|
130
163
|
const patch = {
|
|
131
164
|
commandBus: {
|
|
132
165
|
...cbState,
|
|
133
|
-
|
|
166
|
+
lastAttemptTs: attemptTs,
|
|
167
|
+
consecutiveFailures,
|
|
134
168
|
},
|
|
135
169
|
};
|
|
170
|
+
const nextFloor = backoffSeconds(resolved.minPollIntervalSeconds, consecutiveFailures, resolved.circuitBreakerThreshold, resolved.maxBackoffSeconds);
|
|
136
171
|
logging.info(repoRoot, 'command_bus_poll_failed', {
|
|
137
172
|
hookEvent,
|
|
138
173
|
error: e instanceof Error ? e.message : String(e),
|
|
139
|
-
consecutiveFailures
|
|
174
|
+
consecutiveFailures,
|
|
175
|
+
nextPollAfterSeconds: nextFloor,
|
|
176
|
+
backoff: consecutiveFailures >= resolved.circuitBreakerThreshold,
|
|
140
177
|
});
|
|
141
178
|
return { events: [], statePatch: patch, reactionMode };
|
|
142
179
|
}
|
|
@@ -189,7 +226,9 @@ function resolveConfig(c) {
|
|
|
189
226
|
hooks.add('sessionStart'); // bootstrap is non-removable
|
|
190
227
|
const minPollIntervalSeconds = Math.max(0, c?.minPollIntervalSeconds ?? DEFAULT_MIN_POLL_INTERVAL_SECONDS);
|
|
191
228
|
const reactionMode = c?.reactionMode ?? DEFAULT_REACTION_MODE;
|
|
192
|
-
|
|
229
|
+
const circuitBreakerThreshold = Math.max(1, c?.circuitBreakerThreshold ?? DEFAULT_CIRCUIT_BREAKER_THRESHOLD);
|
|
230
|
+
const maxBackoffSeconds = Math.max(minPollIntervalSeconds, c?.maxBackoffSeconds ?? DEFAULT_MAX_BACKOFF_SECONDS);
|
|
231
|
+
return { enabled, hooks, minPollIntervalSeconds, reactionMode, circuitBreakerThreshold, maxBackoffSeconds };
|
|
193
232
|
}
|
|
194
233
|
function readActiveFeature(repoRoot) {
|
|
195
234
|
try {
|
|
@@ -243,17 +282,27 @@ function readDezycroConfig(repoRoot) {
|
|
|
243
282
|
}
|
|
244
283
|
}
|
|
245
284
|
function readToken(repoRoot) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
285
|
+
// Project-local override first, then the user-global token written by
|
|
286
|
+
// `dezycro-setup` (`~/.dezycro/token.json`). Typical install needs no
|
|
287
|
+
// per-repo file at all.
|
|
288
|
+
const candidates = [
|
|
289
|
+
path.join(repoRoot, '.dezycro', 'companion-token.local.json'),
|
|
290
|
+
path.join(os.homedir(), '.dezycro', 'token.json'),
|
|
291
|
+
];
|
|
292
|
+
for (const p of candidates) {
|
|
293
|
+
try {
|
|
294
|
+
if (!fs.existsSync(p))
|
|
295
|
+
continue;
|
|
296
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
297
|
+
const token = raw.token;
|
|
298
|
+
if (typeof token === 'string' && token.length > 0)
|
|
299
|
+
return token;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Try the next candidate.
|
|
303
|
+
}
|
|
256
304
|
}
|
|
305
|
+
return null;
|
|
257
306
|
}
|
|
258
307
|
async function fetchInbox(apiUrl, feature, token, since) {
|
|
259
308
|
const url = new URL(`${apiUrl}/api/v1/workspaces/${feature.workspaceId}/features/${feature.featureId}/inbox`);
|
|
@@ -304,4 +353,5 @@ exports._internals = {
|
|
|
304
353
|
renderEvent,
|
|
305
354
|
reactionGuidance,
|
|
306
355
|
trimRecent,
|
|
356
|
+
backoffSeconds,
|
|
307
357
|
};
|
package/dist/hooks/lib/router.js
CHANGED
|
@@ -50,6 +50,7 @@ const config = __importStar(require("./config"));
|
|
|
50
50
|
const state = __importStar(require("./state"));
|
|
51
51
|
const logging = __importStar(require("./logging"));
|
|
52
52
|
const verify = __importStar(require("./verify"));
|
|
53
|
+
const publishSpec = __importStar(require("./publish-spec"));
|
|
53
54
|
const controllerMatch = __importStar(require("./controller-match"));
|
|
54
55
|
const authoring = __importStar(require("./authoring"));
|
|
55
56
|
const qualityGate = __importStar(require("./quality-gate"));
|
|
@@ -509,10 +510,57 @@ async function handleUserPromptSubmit(payload) {
|
|
|
509
510
|
}
|
|
510
511
|
}
|
|
511
512
|
}
|
|
513
|
+
// Pillar 2 — persistent publish-spec/verify reminder. The PostToolUse pulse
|
|
514
|
+
// nudge only fires on the exact edit that touches a controller path (and only
|
|
515
|
+
// once pulseCount crosses the threshold), so endpoint changes routinely go
|
|
516
|
+
// un-surfaced. Re-surface on every UserPromptSubmit while needsSpecPublish is
|
|
517
|
+
// set, so the agent is reminded each turn until it publishes the spec. Gated
|
|
518
|
+
// by companion.autoVerify.autoPublishSpecOnControllerChange (default true).
|
|
519
|
+
{
|
|
520
|
+
const specNudge = buildPublishSpecNudge(repoRoot);
|
|
521
|
+
if (specNudge) {
|
|
522
|
+
logging.info(repoRoot, 'publish_spec_nudge', { paths: specNudge.count });
|
|
523
|
+
const out = {
|
|
524
|
+
hookSpecificOutput: {
|
|
525
|
+
hookEventName: 'UserPromptSubmit',
|
|
526
|
+
additionalContext: specNudge.text,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
process.stdout.write(JSON.stringify(out));
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
512
533
|
maybeSurfaceCoverage(repoRoot, current);
|
|
513
534
|
process.stdout.write('{}');
|
|
514
535
|
return 0;
|
|
515
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Build the persistent publish-spec/verify reminder, or null when nothing is
|
|
539
|
+
* pending or the feature is disabled. Surfaced on every UserPromptSubmit while
|
|
540
|
+
* `needsSpecPublish` is set; clears automatically once `/dezycro:publish-spec`
|
|
541
|
+
* resets the flags.
|
|
542
|
+
*/
|
|
543
|
+
function buildPublishSpecNudge(repoRoot) {
|
|
544
|
+
const cfg = safeLoadConfig(repoRoot);
|
|
545
|
+
const av = (cfg && cfg.companion && cfg.companion.autoVerify) || {};
|
|
546
|
+
if (av.autoPublishSpecOnControllerChange === false)
|
|
547
|
+
return null;
|
|
548
|
+
const check = publishSpec.needsPublishBeforeVerify(repoRoot);
|
|
549
|
+
if (!check.needed || check.controllerPaths.length === 0)
|
|
550
|
+
return null;
|
|
551
|
+
const MAX_LISTED = 8;
|
|
552
|
+
const paths = check.controllerPaths;
|
|
553
|
+
const shown = paths.slice(0, MAX_LISTED).map((p) => ' - ' + p);
|
|
554
|
+
if (paths.length > MAX_LISTED)
|
|
555
|
+
shown.push(' - …and ' + (paths.length - MAX_LISTED) + ' more');
|
|
556
|
+
const text = '[Dezycro Companion] ' + paths.length + ' controller/API file(s) changed since the last published spec:\n' +
|
|
557
|
+
shown.join('\n') + '\n\n' +
|
|
558
|
+
'New or changed endpoints are NOT yet reflected in the published OpenAPI spec, so verification ' +
|
|
559
|
+
'cannot see them. Before continuing, run `/dezycro:publish-spec` to upload the updated spec (this ' +
|
|
560
|
+
'regenerates the JEP), then `/dezycro:verify` to check the feature against it. This reminder repeats ' +
|
|
561
|
+
'each turn until the spec is published and clears automatically after `/dezycro:publish-spec`.';
|
|
562
|
+
return { text, count: paths.length };
|
|
563
|
+
}
|
|
516
564
|
function maybeSurfaceCoverage(repoRoot, current) {
|
|
517
565
|
const cfg = safeLoadConfig(repoRoot);
|
|
518
566
|
if (cfg?.companion?.coverageNudges?.enabled === false)
|
package/dist/hooks/lib/state.js
CHANGED
package/dist/hooks/types.d.ts
CHANGED
|
@@ -206,10 +206,25 @@ export interface CommandBusConfig {
|
|
|
206
206
|
pollHooks?: CommandBusHook[];
|
|
207
207
|
minPollIntervalSeconds?: number;
|
|
208
208
|
reactionMode?: CommandBusReactionMode;
|
|
209
|
+
/**
|
|
210
|
+
* After this many consecutive poll failures the effective poll interval grows
|
|
211
|
+
* exponentially (backoff) so an unreachable inbox endpoint stops being polled
|
|
212
|
+
* on every hook fire. Defaults to 3.
|
|
213
|
+
*/
|
|
214
|
+
circuitBreakerThreshold?: number;
|
|
215
|
+
/** Upper bound (seconds) on the backed-off poll interval. Defaults to 1800 (30m). */
|
|
216
|
+
maxBackoffSeconds?: number;
|
|
209
217
|
}
|
|
210
218
|
export interface CommandBusState {
|
|
211
219
|
cursor: string | null;
|
|
220
|
+
/** Timestamp of the last SUCCESSFUL poll (server clock). Used as the dedup/cursor anchor. */
|
|
212
221
|
lastPollTs: string | null;
|
|
222
|
+
/**
|
|
223
|
+
* Timestamp of the last poll ATTEMPT (success or failure, client clock). The
|
|
224
|
+
* min-interval / backoff floor is measured from here so repeated failures are
|
|
225
|
+
* throttled even before a first success exists.
|
|
226
|
+
*/
|
|
227
|
+
lastAttemptTs?: string | null;
|
|
213
228
|
consecutiveFailures: number;
|
|
214
229
|
recentEventIds: string[];
|
|
215
230
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dezycro-ai/agent-plugin",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Official Dezycro plugin for AI coding agents (Claude Code today, more to come) — reverse-engineer your codebase into features, PRDs, TRDs, and live verification.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dezycro-setup": "./setup.js",
|
package/setup.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
4
7
|
const readline = require("readline");
|
|
5
8
|
|
|
6
9
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
10
|
|
|
8
|
-
console.log(`\n Dezycro
|
|
11
|
+
console.log(`\n Dezycro Setup`);
|
|
9
12
|
console.log(` Get your API key from Dezycro → Settings → API Keys\n`);
|
|
10
13
|
|
|
11
14
|
rl.question(" API key (dzy_...): ", (apiKey) => {
|
|
@@ -17,19 +20,34 @@ rl.question(" API key (dzy_...): ", (apiKey) => {
|
|
|
17
20
|
process.exit(1);
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
const
|
|
23
|
+
const mcpUrl = "https://mcp.dezycro.ai";
|
|
21
24
|
|
|
25
|
+
// 1. Configure Claude Code MCP server.
|
|
22
26
|
try {
|
|
23
27
|
execSync(
|
|
24
|
-
`claude mcp add --transport http dezycro "${
|
|
28
|
+
`claude mcp add --transport http dezycro "${mcpUrl}/mcp" --header "Authorization: Bearer ${apiKey}"`,
|
|
25
29
|
{ stdio: "inherit" }
|
|
26
30
|
);
|
|
27
31
|
console.log(`\n ✓ Dezycro MCP configured`);
|
|
28
|
-
console.log(`\n You're all set! In Claude Code run: /dezycro:import-features\n`);
|
|
29
32
|
} catch (err) {
|
|
30
33
|
console.error(`\n Failed to configure MCP. Run manually:`);
|
|
31
|
-
console.error(` claude mcp add --transport http dezycro "${
|
|
34
|
+
console.error(` claude mcp add --transport http dezycro "${mcpUrl}/mcp" --header "Authorization: Bearer ${apiKey}"\n`);
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
// 2. Save the user-global Companion token so companion-runner can poll the
|
|
38
|
+
// Agent Command Bus without per-repo config. Project-local
|
|
39
|
+
// .dezycro/companion-token.local.json still overrides this.
|
|
40
|
+
try {
|
|
41
|
+
const globalDir = path.join(os.homedir(), ".dezycro");
|
|
42
|
+
const globalTokenPath = path.join(globalDir, "token.json");
|
|
43
|
+
fs.mkdirSync(globalDir, { recursive: true, mode: 0o700 });
|
|
44
|
+
fs.writeFileSync(globalTokenPath, JSON.stringify({ token: apiKey }, null, 2), { mode: 0o600 });
|
|
45
|
+
console.log(` ✓ Companion token saved to ${globalTokenPath}`);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(` Failed to save companion token: ${err.message}`);
|
|
48
|
+
console.error(` Drop it manually at ~/.dezycro/token.json: {"token": "${apiKey}"}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`\n You're all set! In Claude Code run: /dezycro:import-features\n`);
|
|
34
52
|
rl.close();
|
|
35
53
|
});
|