agent-skill-manager 1.0.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.
@@ -0,0 +1,83 @@
1
+ import { BoxRenderable, TextRenderable } from "@opentui/core";
2
+ import type { RenderContext } from "@opentui/core";
3
+ import { theme } from "../utils/colors";
4
+ import { VERSION_STRING } from "../utils/version";
5
+
6
+ const KEYBINDINGS = [
7
+ ["\u2191 / k", "Move up"],
8
+ ["\u2193 / j", "Move down"],
9
+ ["Enter", "View skill details"],
10
+ ["d", "Uninstall skill"],
11
+ ["/", "Search / filter"],
12
+ ["Esc", "Back / clear filter"],
13
+ ["Tab", "Cycle scope"],
14
+ ["s", "Cycle sort order"],
15
+ ["r", "Refresh / rescan skills"],
16
+ ["c", "Open configuration"],
17
+ ["?", "Toggle this help"],
18
+ ["q", "Quit"],
19
+ ];
20
+
21
+ export function createHelpView(ctx: RenderContext): BoxRenderable {
22
+ const boxWidth = 44;
23
+ const boxHeight = KEYBINDINGS.length + 7;
24
+ const top = Math.max(0, Math.floor((ctx.height - boxHeight) / 2));
25
+ const left = Math.max(0, Math.floor((ctx.width - boxWidth) / 2));
26
+
27
+ const container = new BoxRenderable(ctx, {
28
+ id: "help-overlay",
29
+ border: true,
30
+ borderStyle: "rounded",
31
+ borderColor: theme.accent,
32
+ backgroundColor: theme.bgAlt,
33
+ title: " Keyboard Shortcuts ",
34
+ titleAlignment: "center",
35
+ padding: 1,
36
+ flexDirection: "column",
37
+ gap: 0,
38
+ width: boxWidth,
39
+ height: boxHeight,
40
+ position: "absolute",
41
+ top,
42
+ left,
43
+ zIndex: 100,
44
+ });
45
+
46
+ for (const [key, action] of KEYBINDINGS) {
47
+ const row = new BoxRenderable(ctx, {
48
+ id: `help-row-${key}`,
49
+ flexDirection: "row",
50
+ width: "100%",
51
+ height: 1,
52
+ });
53
+
54
+ const keyText = new TextRenderable(ctx, {
55
+ content: key.padEnd(12),
56
+ fg: theme.cyan,
57
+ width: 14,
58
+ });
59
+
60
+ const actionText = new TextRenderable(ctx, {
61
+ content: action,
62
+ fg: theme.fg,
63
+ });
64
+
65
+ row.add(keyText);
66
+ row.add(actionText);
67
+ container.add(row);
68
+ }
69
+
70
+ const footer = new TextRenderable(ctx, {
71
+ content: `\n Press ? or Esc to close`,
72
+ fg: theme.fgDim,
73
+ });
74
+ container.add(footer);
75
+
76
+ const versionText = new TextRenderable(ctx, {
77
+ content: ` ${VERSION_STRING}`,
78
+ fg: theme.fgDim,
79
+ });
80
+ container.add(versionText);
81
+
82
+ return container;
83
+ }
@@ -0,0 +1,114 @@
1
+ import { BoxRenderable, TextRenderable } from "@opentui/core";
2
+ import type { RenderContext } from "@opentui/core";
3
+ import { theme } from "../utils/colors";
4
+ import type { SkillInfo } from "../utils/types";
5
+
6
+ function detailRow(
7
+ ctx: RenderContext,
8
+ id: string,
9
+ label: string,
10
+ value: string,
11
+ valueColor: string = theme.fg,
12
+ ): BoxRenderable {
13
+ const row = new BoxRenderable(ctx, {
14
+ id: `detail-row-${id}`,
15
+ flexDirection: "row",
16
+ width: "100%",
17
+ height: 1,
18
+ });
19
+
20
+ const labelText = new TextRenderable(ctx, {
21
+ content: `${label}:`.padEnd(15),
22
+ fg: theme.fgDim,
23
+ width: 16,
24
+ });
25
+
26
+ const valueText = new TextRenderable(ctx, {
27
+ content: value,
28
+ fg: valueColor,
29
+ });
30
+
31
+ row.add(labelText);
32
+ row.add(valueText);
33
+ return row;
34
+ }
35
+
36
+ export function createDetailView(
37
+ ctx: RenderContext,
38
+ skill: SkillInfo,
39
+ ): BoxRenderable {
40
+ const boxWidth = 64;
41
+ const boxHeight = 20;
42
+ const top = Math.max(0, Math.floor((ctx.height - boxHeight) / 2));
43
+ const left = Math.max(0, Math.floor((ctx.width - boxWidth) / 2));
44
+
45
+ const container = new BoxRenderable(ctx, {
46
+ id: "detail-overlay",
47
+ border: true,
48
+ borderStyle: "rounded",
49
+ borderColor: theme.accent,
50
+ backgroundColor: theme.bgAlt,
51
+ title: ` ${skill.name} `,
52
+ titleAlignment: "center",
53
+ padding: 1,
54
+ flexDirection: "column",
55
+ gap: 0,
56
+ width: boxWidth,
57
+ height: boxHeight,
58
+ position: "absolute",
59
+ top,
60
+ left,
61
+ zIndex: 100,
62
+ });
63
+
64
+ container.add(detailRow(ctx, "name", "Name", skill.name, theme.accent));
65
+ container.add(
66
+ detailRow(ctx, "version", "Version", skill.version, theme.green),
67
+ );
68
+ container.add(
69
+ detailRow(
70
+ ctx,
71
+ "provider",
72
+ "Provider",
73
+ skill.providerLabel,
74
+ theme.accentAlt,
75
+ ),
76
+ );
77
+ container.add(
78
+ detailRow(ctx, "location", "Location", skill.location, theme.cyan),
79
+ );
80
+ container.add(detailRow(ctx, "path", "Path", skill.path));
81
+ container.add(
82
+ detailRow(
83
+ ctx,
84
+ "symlink",
85
+ "Symlink",
86
+ skill.isSymlink ? `yes \u2192 ${skill.symlinkTarget}` : "no",
87
+ skill.isSymlink ? theme.yellow : theme.fgDim,
88
+ ),
89
+ );
90
+ container.add(detailRow(ctx, "files", "Files", String(skill.fileCount)));
91
+ container.add(detailRow(ctx, "scope", "Scope", skill.scope, theme.accentAlt));
92
+
93
+ const descLabel = new TextRenderable(ctx, {
94
+ content: "\nDescription:",
95
+ fg: theme.fgDim,
96
+ });
97
+ container.add(descLabel);
98
+
99
+ const desc = skill.description || "(no description)";
100
+ const descText = new TextRenderable(ctx, {
101
+ content: ` ${desc}`,
102
+ fg: theme.fg,
103
+ width: 58,
104
+ });
105
+ container.add(descText);
106
+
107
+ const footer = new TextRenderable(ctx, {
108
+ content: "\n Esc Back d Uninstall",
109
+ fg: theme.fgDim,
110
+ });
111
+ container.add(footer);
112
+
113
+ return container;
114
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ BoxRenderable,
3
+ TextRenderable,
4
+ SelectRenderable,
5
+ SelectRenderableEvents,
6
+ } from "@opentui/core";
7
+ import type { RenderContext } from "@opentui/core";
8
+ import { theme } from "../utils/colors";
9
+ import type { SkillInfo } from "../utils/types";
10
+
11
+ function formatSkillRow(
12
+ index: number,
13
+ skill: SkillInfo,
14
+ descWidth: number,
15
+ ): string {
16
+ const idx = String(index).padStart(3);
17
+ const prefix = skill.isSymlink ? "~ " : " ";
18
+ const nameMax = 24 - prefix.length;
19
+ const rawName =
20
+ skill.name.length > nameMax
21
+ ? skill.name.slice(0, nameMax - 3) + "..."
22
+ : skill.name;
23
+ const name = prefix + rawName;
24
+ const ver =
25
+ skill.version.length > 7 ? skill.version.slice(0, 7) : skill.version;
26
+ const prov =
27
+ skill.providerLabel.length > 11
28
+ ? skill.providerLabel.slice(0, 11)
29
+ : skill.providerLabel;
30
+ const scope = skill.scope;
31
+ const type = skill.isSymlink ? "\u2192link" : " dir ";
32
+ const desc =
33
+ descWidth > 0 ? " " + (skill.description || "").slice(0, descWidth) : "";
34
+ return `${idx} ${name.padEnd(24)} ${ver.padEnd(8)} ${prov.padEnd(12)} ${scope.padEnd(8)} ${type.padEnd(6)}${desc}`;
35
+ }
36
+
37
+ function buildOptions(skills: SkillInfo[], descWidth: number) {
38
+ if (skills.length === 0) {
39
+ return [
40
+ { name: " (no skills found)", description: "", value: "__none__" },
41
+ ];
42
+ }
43
+ return skills.map((s, i) => ({
44
+ name: formatSkillRow(i + 1, s, descWidth),
45
+ description: "",
46
+ value: s.dirName,
47
+ }));
48
+ }
49
+
50
+ function calcDescWidth(termWidth: number): number {
51
+ // 2(border) + 2(padding) + 4(#) + 24(name) + 8(ver) + 12(provider) + 8(scope) + 6(type) + 6(spaces) = 72
52
+ const fixed = 72;
53
+ return Math.max(0, termWidth - fixed);
54
+ }
55
+
56
+ export function createSkillList(
57
+ ctx: RenderContext,
58
+ skills: SkillInfo[],
59
+ onSelect: (skill: SkillInfo) => void,
60
+ termWidth: number = 80,
61
+ ): {
62
+ container: BoxRenderable;
63
+ select: SelectRenderable;
64
+ update: (skills: SkillInfo[], tw?: number) => void;
65
+ } {
66
+ let descWidth = calcDescWidth(termWidth);
67
+
68
+ const container = new BoxRenderable(ctx, {
69
+ id: "skill-list-box",
70
+ border: true,
71
+ borderStyle: "rounded",
72
+ borderColor: theme.border,
73
+ title: ` Skills (${skills.length}) `,
74
+ titleAlignment: "left",
75
+ flexDirection: "column",
76
+ width: "100%",
77
+ flexGrow: 1,
78
+ minHeight: 6,
79
+ });
80
+
81
+ // Header row
82
+ const descHeader = descWidth > 0 ? " Description" : "";
83
+ const headerRow = new TextRenderable(ctx, {
84
+ id: "skill-list-header",
85
+ content: `${"#".padStart(3)} ${"Name".padEnd(26)} ${"Ver".padEnd(8)} ${"Provider".padEnd(12)} ${"Scope".padEnd(8)} ${"Type".padEnd(6)}${descHeader}`,
86
+ fg: theme.fgDim,
87
+ height: 1,
88
+ });
89
+
90
+ const select = new SelectRenderable(ctx, {
91
+ id: "skill-select",
92
+ width: "100%",
93
+ flexGrow: 1,
94
+ options: buildOptions(skills, descWidth),
95
+ wrapSelection: true,
96
+ showScrollIndicator: true,
97
+ fastScrollStep: 5,
98
+ showDescription: false,
99
+ });
100
+
101
+ let currentSkills = skills;
102
+
103
+ (select as any).on(SelectRenderableEvents.ITEM_SELECTED, (index: number) => {
104
+ if (currentSkills[index]) {
105
+ onSelect(currentSkills[index]);
106
+ }
107
+ });
108
+
109
+ container.add(headerRow);
110
+ container.add(select);
111
+
112
+ function update(newSkills: SkillInfo[], tw?: number) {
113
+ if (tw !== undefined) {
114
+ descWidth = calcDescWidth(tw);
115
+ }
116
+ currentSkills = newSkills;
117
+ select.options = buildOptions(newSkills, descWidth);
118
+ container.title = ` Skills (${newSkills.length}) `;
119
+ }
120
+
121
+ return { container, select, update };
122
+ }