@danieljvdm/dev-kit 0.4.0 → 0.6.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 +130 -35
- package/dev-kit.example.jsonc +2 -0
- package/package.json +3 -2
- package/schema/dev-kit.schema.json +32 -4
- package/skills/dev-kit/SKILL.md +31 -6
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/src/bin/dev-kit.ts +5 -5
- package/src/catalog.ts +73 -16
- package/src/index.ts +14 -0
- package/src/manifest.ts +29 -2
- package/src/package-skill-source.ts +250 -0
- package/src/path-digest.ts +7 -0
- package/src/project-package.ts +34 -0
- package/src/project-state.ts +50 -9
- package/src/skill-manager.ts +66 -30
- package/src/skill-selector.ts +43 -0
- package/src/sync.ts +279 -40
- package/templates/AGENTS.md +9 -0
package/src/path-digest.ts
CHANGED
|
@@ -135,6 +135,13 @@ export const digestText = Effect.fn("digestText")(function* (value: string) {
|
|
|
135
135
|
return yield* digestFrames(["text-v1", value]);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
export const digestFileContent = Effect.fn("digestFileContent")(function* (
|
|
139
|
+
value: string,
|
|
140
|
+
mode = 0o644,
|
|
141
|
+
) {
|
|
142
|
+
return yield* digestFrames(["file-v1", String(mode), value]);
|
|
143
|
+
});
|
|
144
|
+
|
|
138
145
|
export const digestSymlinkTarget = Effect.fn("digestSymlinkTarget")(function* (target: string) {
|
|
139
146
|
return yield* digestFrames(["symlink-v1", target]);
|
|
140
147
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class ProjectPackageError extends Schema.TaggedErrorClass<ProjectPackageError>()(
|
|
4
|
+
"ProjectPackageError",
|
|
5
|
+
{ message: Schema.String },
|
|
6
|
+
) {}
|
|
7
|
+
|
|
8
|
+
const ProjectPackageSchema = Schema.fromJsonString(Schema.Struct({
|
|
9
|
+
dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
10
|
+
devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
11
|
+
optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
12
|
+
peerDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
export const readDirectDependencyNames = Effect.fn("readDirectDependencyNames")(function* (
|
|
16
|
+
projectDir: string,
|
|
17
|
+
) {
|
|
18
|
+
const fs = yield* FileSystem.FileSystem;
|
|
19
|
+
const path = yield* Path.Path;
|
|
20
|
+
const manifestPath = path.join(projectDir, "package.json");
|
|
21
|
+
if (!(yield* fs.exists(manifestPath))) return [];
|
|
22
|
+
const manifest = yield* fs.readFileString(manifestPath).pipe(
|
|
23
|
+
Effect.flatMap(Schema.decodeUnknownEffect(ProjectPackageSchema)),
|
|
24
|
+
Effect.mapError(() => new ProjectPackageError({
|
|
25
|
+
message: `invalid project package.json: ${manifestPath}`,
|
|
26
|
+
})),
|
|
27
|
+
);
|
|
28
|
+
return [...new Set([
|
|
29
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
30
|
+
...Object.keys(manifest.devDependencies ?? {}),
|
|
31
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
32
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
33
|
+
])].sort();
|
|
34
|
+
});
|
package/src/project-state.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
import { DigestSchema } from "./path-digest.ts";
|
|
4
4
|
|
|
5
|
+
export const CatalogProvenanceSchema = Schema.Union([
|
|
6
|
+
Schema.Struct({
|
|
7
|
+
source: Schema.String,
|
|
8
|
+
repository: Schema.String,
|
|
9
|
+
resolved: Schema.String,
|
|
10
|
+
}),
|
|
11
|
+
Schema.Struct({
|
|
12
|
+
package: Schema.String,
|
|
13
|
+
version: Schema.String,
|
|
14
|
+
skill: Schema.String,
|
|
15
|
+
digest: DigestSchema,
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
export type CatalogProvenance = typeof CatalogProvenanceSchema.Type;
|
|
19
|
+
|
|
5
20
|
export const ManagedSkillOutputSchema = Schema.Struct({
|
|
6
21
|
resourceId: Schema.String,
|
|
7
22
|
path: Schema.String,
|
|
@@ -10,16 +25,42 @@ export const ManagedSkillOutputSchema = Schema.Struct({
|
|
|
10
25
|
mode: Schema.Literals(["copy", "symlink"]),
|
|
11
26
|
kind: Schema.Literals(["directory", "symlink"]),
|
|
12
27
|
digest: DigestSchema,
|
|
13
|
-
catalog: Schema.optional(
|
|
14
|
-
Schema.Struct({
|
|
15
|
-
source: Schema.String,
|
|
16
|
-
repository: Schema.String,
|
|
17
|
-
resolved: Schema.String,
|
|
18
|
-
}),
|
|
19
|
-
),
|
|
28
|
+
catalog: Schema.optional(CatalogProvenanceSchema),
|
|
20
29
|
});
|
|
21
30
|
export type ManagedSkillOutput = typeof ManagedSkillOutputSchema.Type;
|
|
22
31
|
|
|
32
|
+
export const ManagedAgentInstructionsOutputSchema = Schema.Struct({
|
|
33
|
+
resourceId: Schema.Literal("setup:agent-instructions"),
|
|
34
|
+
path: Schema.String,
|
|
35
|
+
sourcePath: Schema.String,
|
|
36
|
+
mode: Schema.Literal("copy"),
|
|
37
|
+
kind: Schema.Literal("file"),
|
|
38
|
+
digest: DigestSchema,
|
|
39
|
+
});
|
|
40
|
+
export type ManagedAgentInstructionsOutput = typeof ManagedAgentInstructionsOutputSchema.Type;
|
|
41
|
+
|
|
42
|
+
export const ManagedClaudeInstructionsOutputSchema = Schema.Struct({
|
|
43
|
+
resourceId: Schema.Literal("setup:claude-instructions"),
|
|
44
|
+
path: Schema.String,
|
|
45
|
+
sourcePath: Schema.String,
|
|
46
|
+
mode: Schema.Literal("symlink"),
|
|
47
|
+
kind: Schema.Literal("symlink"),
|
|
48
|
+
digest: DigestSchema,
|
|
49
|
+
});
|
|
50
|
+
export type ManagedClaudeInstructionsOutput = typeof ManagedClaudeInstructionsOutputSchema.Type;
|
|
51
|
+
|
|
52
|
+
export const ManagedInstructionOutputSchema = Schema.Union([
|
|
53
|
+
ManagedAgentInstructionsOutputSchema,
|
|
54
|
+
ManagedClaudeInstructionsOutputSchema,
|
|
55
|
+
]);
|
|
56
|
+
export type ManagedInstructionOutput = typeof ManagedInstructionOutputSchema.Type;
|
|
57
|
+
|
|
58
|
+
export const ManagedOutputSchema = Schema.Union([
|
|
59
|
+
ManagedSkillOutputSchema,
|
|
60
|
+
ManagedInstructionOutputSchema,
|
|
61
|
+
]);
|
|
62
|
+
export type ManagedOutput = typeof ManagedOutputSchema.Type;
|
|
63
|
+
|
|
23
64
|
export const EffectTsgoLockSchema = Schema.Struct({
|
|
24
65
|
effectTsgoVersion: Schema.String,
|
|
25
66
|
typescriptPackage: Schema.String,
|
|
@@ -46,7 +87,7 @@ export const DevKitLockSchema = Schema.Struct({
|
|
|
46
87
|
effectTsgo: Schema.optional(EffectTsgoLockSchema),
|
|
47
88
|
}),
|
|
48
89
|
),
|
|
49
|
-
outputs: Schema.Array(
|
|
90
|
+
outputs: Schema.Array(ManagedOutputSchema),
|
|
50
91
|
});
|
|
51
92
|
export type DevKitLock = typeof DevKitLockSchema.Type;
|
|
52
93
|
|
|
@@ -54,7 +95,7 @@ export const OwnershipReceiptSchema = Schema.Struct({
|
|
|
54
95
|
resourceId: Schema.String,
|
|
55
96
|
path: Schema.String,
|
|
56
97
|
mode: Schema.Literals(["copy", "symlink"]),
|
|
57
|
-
kind: Schema.Literals(["directory", "symlink"]),
|
|
98
|
+
kind: Schema.Literals(["file", "directory", "symlink"]),
|
|
58
99
|
digest: DigestSchema,
|
|
59
100
|
});
|
|
60
101
|
export type OwnershipReceipt = typeof OwnershipReceiptSchema.Type;
|
package/src/skill-manager.ts
CHANGED
|
@@ -188,8 +188,14 @@ const selectedNames = (
|
|
|
188
188
|
return selected;
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
-
const
|
|
192
|
-
|
|
191
|
+
const displayValue = (value: string): string =>
|
|
192
|
+
[...value].map((character) => {
|
|
193
|
+
const code = character.charCodeAt(0);
|
|
194
|
+
return code <= 31 || (code >= 127 && code <= 159) ? " " : character;
|
|
195
|
+
}).join("").replace(/\s+/g, " ").trim();
|
|
196
|
+
|
|
197
|
+
const summary = (description: string, defaultDescription: string): string => {
|
|
198
|
+
const text = displayValue(description || defaultDescription);
|
|
193
199
|
const firstSentence = text.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() ?? text;
|
|
194
200
|
return firstSentence.length > 96 ? `${firstSentence.slice(0, 93).trimEnd()}…` : firstSentence;
|
|
195
201
|
};
|
|
@@ -219,15 +225,16 @@ export const addSkills = Effect.fn("addManagedSkills")(function* (
|
|
|
219
225
|
options: ManagerOptions,
|
|
220
226
|
) {
|
|
221
227
|
const current = yield* readManifest(options, true);
|
|
222
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
223
|
-
const known = new Set([...catalog.skills.map((skill) => skill.
|
|
228
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
229
|
+
const known = new Set([...catalog.skills.map((skill) => skill.selector), ...Object.keys(catalog.families)]);
|
|
224
230
|
const unknown = names.filter((name) => !known.has(name));
|
|
225
231
|
if (unknown.length > 0) {
|
|
226
232
|
return yield* new SkillManagerError({
|
|
227
233
|
message: `unknown skill${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Try \`dev-kit search ${unknown[0]}\`.`,
|
|
228
234
|
});
|
|
229
235
|
}
|
|
230
|
-
|
|
236
|
+
const sourceFamilies = catalog.lock?.sources ?? [];
|
|
237
|
+
for (const source of sourceFamilies) {
|
|
231
238
|
if (!names.includes(source.id)) continue;
|
|
232
239
|
yield* printStatus(
|
|
233
240
|
"info",
|
|
@@ -255,7 +262,7 @@ export const removeSkills = Effect.fn("removeManagedSkills")(function* (
|
|
|
255
262
|
options: ManagerOptions,
|
|
256
263
|
) {
|
|
257
264
|
const current = yield* readManifest(options);
|
|
258
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
265
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
259
266
|
const before = selectedNames(
|
|
260
267
|
current.manifest.include,
|
|
261
268
|
current.manifest.exclude ?? [],
|
|
@@ -287,43 +294,63 @@ export const listSkills = Effect.fn("listManagedSkills")(function* (
|
|
|
287
294
|
) {
|
|
288
295
|
const fs = yield* FileSystem.FileSystem;
|
|
289
296
|
const paths = yield* resolvePaths(options);
|
|
290
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
297
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
|
|
291
298
|
const manifest = (yield* fs.exists(paths.manifestPath))
|
|
292
299
|
? (yield* readManifest(options)).manifest
|
|
293
300
|
: { include: [], exclude: [] };
|
|
294
301
|
const selected = selectedNames(manifest.include, manifest.exclude ?? [], catalog.families);
|
|
295
302
|
const query = options.query?.toLowerCase();
|
|
296
303
|
const visible = catalog.skills.filter((skill) =>
|
|
297
|
-
(options.all || selected.has(skill.
|
|
298
|
-
(!query || `${skill.
|
|
304
|
+
(options.all || selected.has(skill.selector)) &&
|
|
305
|
+
(!query || `${skill.selector} ${skill.description} ${skill.source}`.toLowerCase().includes(query)),
|
|
306
|
+
);
|
|
307
|
+
const catalogSelectors = new Set(catalog.skills.map((skill) => skill.selector));
|
|
308
|
+
const unavailable = [...selected].filter((selector) =>
|
|
309
|
+
!catalogSelectors.has(selector) && (!query || selector.toLowerCase().includes(query))
|
|
299
310
|
);
|
|
300
|
-
if (visible.length === 0) {
|
|
311
|
+
if (visible.length === 0 && unavailable.length === 0) {
|
|
301
312
|
yield* printStatus("info", query ? "No matching skills" : "No skills selected");
|
|
302
313
|
if (!query && !options.all) yield* printDetail("Browse with: dev-kit list --all");
|
|
303
314
|
return;
|
|
304
315
|
}
|
|
305
316
|
for (const skill of visible) {
|
|
306
|
-
const marker = selected.has(skill.
|
|
317
|
+
const marker = selected.has(skill.selector) ? "✓" : " ";
|
|
307
318
|
const origin = skill.bundled ? "built in" : skill.source;
|
|
308
|
-
const provenance = skill.
|
|
309
|
-
|
|
319
|
+
const provenance = skill.package
|
|
320
|
+
? ` [installed ${displayValue(skill.package.version)}]`
|
|
321
|
+
: skill.bundled ? "" : ` [${skill.source}]`;
|
|
322
|
+
yield* printLine(`${marker} ${skill.selector}${provenance} ${summary(skill.description, origin)}`);
|
|
323
|
+
}
|
|
324
|
+
for (const selector of unavailable) {
|
|
325
|
+
yield* printLine(`! ${selector} [unavailable] install or repair the selected direct dependency`);
|
|
310
326
|
}
|
|
311
327
|
yield* printLine();
|
|
312
|
-
yield* printLine(`${selected.size} selected · ${catalog.skills.length}
|
|
328
|
+
yield* printLine(`${selected.size} selected · ${catalog.skills.length} available`);
|
|
313
329
|
});
|
|
314
330
|
|
|
315
|
-
export const showSkill = Effect.fn("showCatalogSkill")(function* (
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
export const showSkill = Effect.fn("showCatalogSkill")(function* (
|
|
332
|
+
name: string,
|
|
333
|
+
options: ManagerOptions,
|
|
334
|
+
) {
|
|
335
|
+
const paths = yield* resolvePaths(options);
|
|
336
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
|
|
337
|
+
const skill = catalog.skills.find((candidate) => candidate.selector === name);
|
|
318
338
|
if (!skill) return yield* new SkillManagerError({ message: `unknown skill: ${name}` });
|
|
319
|
-
yield* printLine(skill.
|
|
320
|
-
if (skill.description) yield* printLine(skill.description);
|
|
339
|
+
yield* printLine(skill.selector);
|
|
340
|
+
if (skill.description) yield* printLine(displayValue(skill.description));
|
|
341
|
+
if (skill.package) {
|
|
342
|
+
yield* printLine(`Source: installed package`);
|
|
343
|
+
yield* printLine(`Package: ${skill.package.name}`);
|
|
344
|
+
yield* printLine(`Version: ${displayValue(skill.package.version)}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
321
347
|
yield* printLine(`Source: ${skill.bundled ? "dev-kit (built in)" : skill.source}`);
|
|
322
348
|
if (!skill.bundled) {
|
|
323
349
|
const source = catalog.lock?.sources.find((candidate) => candidate.id === skill.source);
|
|
324
350
|
if (source) {
|
|
325
351
|
yield* printLine(`Repository: ${source.repository}`);
|
|
326
352
|
yield* printLine(`Approved commit: ${source.resolved}`);
|
|
353
|
+
return;
|
|
327
354
|
}
|
|
328
355
|
}
|
|
329
356
|
});
|
|
@@ -345,22 +372,22 @@ export const chooseSkillsToAdd = Effect.fn("chooseSkillsToAdd")(function* (
|
|
|
345
372
|
return yield* new SkillManagerError({ message: "pass one or more skill names, or run this command in a terminal" });
|
|
346
373
|
}
|
|
347
374
|
const current = yield* readManifest(options, true);
|
|
348
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
375
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
349
376
|
const selected = selectedNames(
|
|
350
377
|
current.manifest.include,
|
|
351
378
|
current.manifest.exclude ?? [],
|
|
352
379
|
catalog.families,
|
|
353
380
|
);
|
|
354
|
-
const available = catalog.skills.filter((skill) => !selected.has(skill.
|
|
381
|
+
const available = catalog.skills.filter((skill) => !selected.has(skill.selector));
|
|
355
382
|
if (available.length === 0) {
|
|
356
|
-
yield* printStatus("success", "All
|
|
383
|
+
yield* printStatus("success", "All available skills are selected");
|
|
357
384
|
return;
|
|
358
385
|
}
|
|
359
386
|
const names = yield* Prompt.multiSelect({
|
|
360
387
|
message: "Choose skills to add",
|
|
361
388
|
choices: available.map((skill) => ({
|
|
362
|
-
title: skill.
|
|
363
|
-
value: skill.
|
|
389
|
+
title: skill.selector,
|
|
390
|
+
value: skill.selector,
|
|
364
391
|
description: summary(skill.description, skill.source),
|
|
365
392
|
})),
|
|
366
393
|
min: 1,
|
|
@@ -375,7 +402,7 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
|
|
|
375
402
|
return yield* new SkillManagerError({ message: "pass one or more skill names, or run this command in a terminal" });
|
|
376
403
|
}
|
|
377
404
|
const current = yield* readManifest(options);
|
|
378
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
405
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
379
406
|
const selected = selectedNames(
|
|
380
407
|
current.manifest.include,
|
|
381
408
|
current.manifest.exclude ?? [],
|
|
@@ -387,11 +414,20 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
|
|
|
387
414
|
}
|
|
388
415
|
const names = yield* Prompt.multiSelect({
|
|
389
416
|
message: "Choose skills to remove",
|
|
390
|
-
choices:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
417
|
+
choices: [
|
|
418
|
+
...catalog.skills.filter((skill) => selected.has(skill.selector)).map((skill) => ({
|
|
419
|
+
title: skill.selector,
|
|
420
|
+
value: skill.selector,
|
|
421
|
+
description: summary(skill.description, skill.source),
|
|
422
|
+
})),
|
|
423
|
+
...[...selected]
|
|
424
|
+
.filter((selector) => !catalog.skills.some((skill) => skill.selector === selector))
|
|
425
|
+
.map((selector) => ({
|
|
426
|
+
title: selector,
|
|
427
|
+
value: selector,
|
|
428
|
+
description: "Selected but currently unavailable",
|
|
429
|
+
})),
|
|
430
|
+
],
|
|
395
431
|
min: 1,
|
|
396
432
|
});
|
|
397
433
|
yield* removeSkills(names, options);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
2
|
+
|
|
3
|
+
/** An immediate Agent Skill directory name. */
|
|
4
|
+
export const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5
|
+
|
|
6
|
+
const PACKAGE_SKILL_SELECTOR_PATTERN = /^(?<package>[^#]+)#(?<skill>[^#]+)$/;
|
|
7
|
+
|
|
8
|
+
type StaticSkillSelector = {
|
|
9
|
+
readonly type: "static";
|
|
10
|
+
readonly name: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PackageSkillSelector = {
|
|
14
|
+
readonly type: "package";
|
|
15
|
+
readonly package: string;
|
|
16
|
+
readonly skill: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SkillSelector = StaticSkillSelector | PackageSkillSelector;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The JSON-schema-compatible pattern for static selectors and exact package
|
|
23
|
+
* selectors. Package names intentionally use the same rules as package-backed
|
|
24
|
+
* skill sources.
|
|
25
|
+
*/
|
|
26
|
+
export const SKILL_SELECTOR_PATTERN =
|
|
27
|
+
/^(?:[a-z0-9]+(?:-[a-z0-9]+)*|(?:[a-z0-9][a-z0-9._-]*|@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*)#[a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
28
|
+
|
|
29
|
+
export const isSkillName = (value: string): boolean => SKILL_NAME_PATTERN.test(value);
|
|
30
|
+
|
|
31
|
+
/** Parse an exact, canonical manifest skill selector. */
|
|
32
|
+
export const parseSkillSelector = (value: string): SkillSelector | undefined => {
|
|
33
|
+
if (isSkillName(value)) return { type: "static", name: value };
|
|
34
|
+
|
|
35
|
+
const match = PACKAGE_SKILL_SELECTOR_PATTERN.exec(value);
|
|
36
|
+
const packageName = match?.groups?.package;
|
|
37
|
+
const skill = match?.groups?.skill;
|
|
38
|
+
if (packageName === undefined || skill === undefined ||
|
|
39
|
+
!isTypeScriptPackageName(packageName) || !isSkillName(skill)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return { type: "package", package: packageName, skill };
|
|
43
|
+
};
|