@nseng-ai/areg 0.1.1
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/package.json +29 -0
- package/src/cli.ts +109 -0
- package/src/context.ts +59 -0
- package/src/fake-gateways.ts +1009 -0
- package/src/gateways/command-constants.ts +1 -0
- package/src/gateways/errors.ts +5 -0
- package/src/gateways/fs-utils.ts +26 -0
- package/src/gateways/github-gateway.ts +111 -0
- package/src/gateways/host-gateway.ts +35 -0
- package/src/gateways/mutation-policy.ts +94 -0
- package/src/gateways/npx-skills-gateway.ts +53 -0
- package/src/gateways/project-fs.ts +320 -0
- package/src/gateways/project-gateway.ts +801 -0
- package/src/gateways/prompt-gateway.ts +21 -0
- package/src/gateways/skill-kind-classification.ts +39 -0
- package/src/gateways/skillx-workspace-gateway.ts +220 -0
- package/src/gateways.ts +361 -0
- package/src/index.ts +48 -0
- package/src/operations/check.ts +559 -0
- package/src/operations/doctor-skills-report.ts +109 -0
- package/src/operations/doctor-skills-severity.ts +9 -0
- package/src/operations/doctor-skills.ts +471 -0
- package/src/operations/file-state.ts +77 -0
- package/src/operations/frontmatter.ts +151 -0
- package/src/operations/init.ts +548 -0
- package/src/operations/lockfile.ts +107 -0
- package/src/operations/managed-markdown-block.ts +47 -0
- package/src/operations/pi-replacement.ts +33 -0
- package/src/operations/pi-settings.ts +60 -0
- package/src/operations/project-agents.ts +115 -0
- package/src/operations/project-inspection.ts +165 -0
- package/src/operations/project-mutations.ts +383 -0
- package/src/operations/project-resolution.ts +53 -0
- package/src/operations/skill-find.ts +295 -0
- package/src/operations/skill-kind-apply-plan.ts +531 -0
- package/src/operations/skill-kind-frontmatter.ts +55 -0
- package/src/operations/skill-kind-inference.ts +391 -0
- package/src/operations/skill-kind.ts +525 -0
- package/src/operations/skill-mirror-conventions.ts +126 -0
- package/src/operations/skillx.ts +305 -0
- package/src/operations/toml-section.ts +53 -0
- package/src/operations/update-skills.ts +325 -0
- package/src/real-gateways.ts +6 -0
- package/src/skill-lookup.ts +254 -0
- package/src/sort.ts +7 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { commandBackedSkillSurface } from "@nseng-ai/command-backed-skill-registry";
|
|
2
|
+
|
|
3
|
+
export interface PiReplacementFacts {
|
|
4
|
+
verifiedSurfaces: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PiReplacementVerification {
|
|
8
|
+
verified: boolean;
|
|
9
|
+
surface?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function verifyPiReplacement(
|
|
13
|
+
skillName: string,
|
|
14
|
+
facts: PiReplacementFacts,
|
|
15
|
+
): PiReplacementVerification {
|
|
16
|
+
const surface = commandBackedSkillSurface(skillName);
|
|
17
|
+
if (surface === undefined) return { verified: false };
|
|
18
|
+
return { verified: facts.verifiedSurfaces.includes(surface), surface };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatReplacementLabel(replacement: PiReplacementVerification): string {
|
|
22
|
+
const prefix = replacement.verified ? "replacement-verified" : "replacement-missing";
|
|
23
|
+
return replacement.surface === undefined ? prefix : `${prefix}:${replacement.surface}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function replacementAdvice(skillName: string, surface: string | undefined): string {
|
|
27
|
+
const expected = surface === undefined ? "a replacement Pi command" : `/${surface}`;
|
|
28
|
+
return [
|
|
29
|
+
`Skill '${skillName}' would hide /skill:${skillName} in Pi, but ${expected} is not verified.`,
|
|
30
|
+
`Add a replacement command that resolves '${skillName}' with shared skill lookup / areg skill find semantics, then reads the returned preferred SKILL.md path because native Pi skill discovery will exclude /skill:${skillName}.`,
|
|
31
|
+
"Add tests proving the command works while the backing skill is excluded.",
|
|
32
|
+
].join(" ");
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { formatErrorMessage, isRecord } from "@nseng-ai/foundation/primitives";
|
|
2
|
+
import { err, type Result } from "@nseng-ai/foundation/result";
|
|
3
|
+
|
|
4
|
+
import type { AregPathState, AregTextFileState } from "../gateways.ts";
|
|
5
|
+
import { rejectTextState, validateOptionalDirectoryState } from "./file-state.ts";
|
|
6
|
+
|
|
7
|
+
export interface PiSettingsData {
|
|
8
|
+
text: string | undefined;
|
|
9
|
+
data: Record<string, unknown> | undefined;
|
|
10
|
+
exclusions: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ParsePiSettingsResult = Result<PiSettingsData>;
|
|
14
|
+
|
|
15
|
+
export function parsePiSettings(
|
|
16
|
+
piDir: AregPathState,
|
|
17
|
+
settings: AregTextFileState,
|
|
18
|
+
): ParsePiSettingsResult {
|
|
19
|
+
const piDirectory = validateOptionalDirectoryState({
|
|
20
|
+
pathLabel: ".pi",
|
|
21
|
+
state: piDir,
|
|
22
|
+
action: "inspect Pi settings",
|
|
23
|
+
});
|
|
24
|
+
if (!piDirectory.ok) return piDirectory;
|
|
25
|
+
if (settings.type === "missing")
|
|
26
|
+
return { ok: true, value: { text: undefined, data: undefined, exclusions: [] } };
|
|
27
|
+
if (settings.type !== "file")
|
|
28
|
+
return rejectTextState({
|
|
29
|
+
pathLabel: ".pi/settings.json",
|
|
30
|
+
state: settings,
|
|
31
|
+
action: "inspect Pi settings",
|
|
32
|
+
unreadableMode: "not-file",
|
|
33
|
+
});
|
|
34
|
+
let data: unknown;
|
|
35
|
+
try {
|
|
36
|
+
data = JSON.parse(settings.text);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return err({
|
|
39
|
+
code: "pi_settings_invalid_json",
|
|
40
|
+
message: `Invalid JSON in .pi/settings.json: ${formatErrorMessage(error)}.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(data))
|
|
44
|
+
return err({
|
|
45
|
+
code: "pi_settings_not_object",
|
|
46
|
+
message: ".pi/settings.json must contain a JSON object.",
|
|
47
|
+
});
|
|
48
|
+
if (data.skills === undefined)
|
|
49
|
+
return { ok: true, value: { text: settings.text, data, exclusions: [] } };
|
|
50
|
+
if (!isStringArray(data.skills))
|
|
51
|
+
return err({
|
|
52
|
+
code: "pi_settings_skills_not_string_array",
|
|
53
|
+
message: ".pi/settings.json field 'skills' must be an array of strings.",
|
|
54
|
+
});
|
|
55
|
+
return { ok: true, value: { text: settings.text, data, exclusions: data.skills } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
59
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
60
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { formatErrorMessage, isRecord } from "@nseng-ai/foundation/primitives";
|
|
2
|
+
import { err, type Result } from "@nseng-ai/foundation/result";
|
|
3
|
+
import { parse } from "smol-toml";
|
|
4
|
+
|
|
5
|
+
import type { AregTextFileState } from "../gateways.ts";
|
|
6
|
+
import { rejectTextState } from "./file-state.ts";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_AGENTS = ["codex", "claude-code"] as const;
|
|
9
|
+
|
|
10
|
+
export function resolveProjectAgents(input: {
|
|
11
|
+
explicitAgents: readonly string[];
|
|
12
|
+
nsToml: AregTextFileState;
|
|
13
|
+
aregJson: AregTextFileState;
|
|
14
|
+
}): Result<string[]> {
|
|
15
|
+
if (input.explicitAgents.length > 0) return { ok: true, value: [...input.explicitAgents] };
|
|
16
|
+
const nsAgents = parseNsAregAgentsFromState(input.nsToml);
|
|
17
|
+
if (!nsAgents.ok) return nsAgents;
|
|
18
|
+
if (nsAgents.value.length > 0) return nsAgents;
|
|
19
|
+
const legacyAgents = parseLegacyAregJsonAgentsFromState(input.aregJson);
|
|
20
|
+
if (!legacyAgents.ok) return legacyAgents;
|
|
21
|
+
if (legacyAgents.value.length > 0) return legacyAgents;
|
|
22
|
+
return { ok: true, value: [...DEFAULT_AGENTS] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseNsAregAgents(text: string, pathLabel = "ns.toml"): Result<string[]> {
|
|
26
|
+
let data: unknown;
|
|
27
|
+
try {
|
|
28
|
+
data = parse(text);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return err({
|
|
31
|
+
code: "ns_toml_invalid",
|
|
32
|
+
message: `Invalid TOML in ${pathLabel}: ${formatErrorMessage(error)}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!isRecord(data)) return { ok: true, value: [] };
|
|
36
|
+
const areg = data.areg;
|
|
37
|
+
if (areg === undefined) return { ok: true, value: [] };
|
|
38
|
+
if (!isRecord(areg))
|
|
39
|
+
return err({
|
|
40
|
+
code: "ns_toml_invalid",
|
|
41
|
+
message: `[areg] in ${pathLabel} must be a TOML table.`,
|
|
42
|
+
});
|
|
43
|
+
const agents = areg.agents;
|
|
44
|
+
if (agents === undefined) return { ok: true, value: [] };
|
|
45
|
+
if (!Array.isArray(agents))
|
|
46
|
+
return err({
|
|
47
|
+
code: "ns_toml_invalid",
|
|
48
|
+
message: `${pathLabel} [areg].agents must be a string array.`,
|
|
49
|
+
});
|
|
50
|
+
if (agents.length === 0) return { ok: true, value: [] };
|
|
51
|
+
return validateNonEmptyStringList(agents, {
|
|
52
|
+
code: "ns_toml_invalid",
|
|
53
|
+
message: `${pathLabel} [areg].agents must be a non-empty string list.`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseLegacyAregJsonAgents(text: string): Result<string[]> {
|
|
58
|
+
let data: unknown;
|
|
59
|
+
try {
|
|
60
|
+
data = JSON.parse(text);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return err({
|
|
63
|
+
code: "areg_json_invalid",
|
|
64
|
+
message: `Invalid JSON in areg.json: ${formatErrorMessage(error)}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!isRecord(data))
|
|
68
|
+
return err({ code: "areg_json_invalid", message: "areg.json must contain a JSON object." });
|
|
69
|
+
const agents = data.agents;
|
|
70
|
+
if (!Array.isArray(agents) || agents.length === 0)
|
|
71
|
+
return err({
|
|
72
|
+
code: "areg_agents_invalid",
|
|
73
|
+
message: "areg.json field `agents` must be a non-empty string list.",
|
|
74
|
+
});
|
|
75
|
+
return validateNonEmptyStringList(agents, {
|
|
76
|
+
code: "areg_agents_invalid",
|
|
77
|
+
message: "areg.json field `agents` must be a non-empty string list.",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateNonEmptyStringList(
|
|
82
|
+
agents: readonly unknown[],
|
|
83
|
+
invalid: { code: string; message: string },
|
|
84
|
+
): Result<string[]> {
|
|
85
|
+
const result: string[] = [];
|
|
86
|
+
for (const agent of agents) {
|
|
87
|
+
if (typeof agent !== "string" || agent.trim().length === 0) return err(invalid);
|
|
88
|
+
result.push(agent);
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, value: result };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseNsAregAgentsFromState(state: AregTextFileState): Result<string[]> {
|
|
94
|
+
if (state.type === "missing") return { ok: true, value: [] };
|
|
95
|
+
if (state.type !== "file")
|
|
96
|
+
return rejectTextState({
|
|
97
|
+
pathLabel: "ns.toml",
|
|
98
|
+
state,
|
|
99
|
+
description: "ns.toml",
|
|
100
|
+
action: "manage it",
|
|
101
|
+
});
|
|
102
|
+
return parseNsAregAgents(state.text, "ns.toml");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseLegacyAregJsonAgentsFromState(state: AregTextFileState): Result<string[]> {
|
|
106
|
+
if (state.type === "missing") return { ok: true, value: [] };
|
|
107
|
+
if (state.type !== "file")
|
|
108
|
+
return rejectTextState({
|
|
109
|
+
pathLabel: "areg.json",
|
|
110
|
+
state,
|
|
111
|
+
description: "areg.json",
|
|
112
|
+
action: "manage it",
|
|
113
|
+
});
|
|
114
|
+
return parseLegacyAregJsonAgents(state.text);
|
|
115
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { AregCliContext } from "../context.ts";
|
|
2
|
+
import type {
|
|
3
|
+
AregCheckPairingDirectory,
|
|
4
|
+
AregCheckSkillInspection,
|
|
5
|
+
AregInstructionFilesInspection,
|
|
6
|
+
AregPathState,
|
|
7
|
+
AregProjectBaseInspection,
|
|
8
|
+
AregReplacementInspection,
|
|
9
|
+
AregSkillKindSkillInspection,
|
|
10
|
+
AregSkillNameInventory,
|
|
11
|
+
AregTextFileState,
|
|
12
|
+
} from "../gateways.ts";
|
|
13
|
+
import { uniqueSortedStrings } from "../sort.ts";
|
|
14
|
+
import { parseInspectedLockfile } from "./lockfile.ts";
|
|
15
|
+
|
|
16
|
+
export interface AregProjectInspectionFacts extends AregProjectBaseInspection {
|
|
17
|
+
piDir: AregPathState;
|
|
18
|
+
piSettings: AregTextFileState;
|
|
19
|
+
replacement: AregReplacementInspection;
|
|
20
|
+
skillInventory: AregSkillNameInventory;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AregCheckProjectInspection {
|
|
24
|
+
projectDir: string;
|
|
25
|
+
projectPathState: AregPathState;
|
|
26
|
+
lockfile: AregTextFileState;
|
|
27
|
+
skillsDirectoryNames: readonly string[];
|
|
28
|
+
agentsSkillNames: readonly string[];
|
|
29
|
+
excludedSkillNames: readonly string[];
|
|
30
|
+
piDir: AregPathState;
|
|
31
|
+
piSettings: AregTextFileState;
|
|
32
|
+
replacement: AregReplacementInspection;
|
|
33
|
+
skills: readonly AregCheckSkillInspection[];
|
|
34
|
+
pairingDirectories: readonly AregCheckPairingDirectory[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AregSkillKindProjectInspection {
|
|
38
|
+
projectDir: string;
|
|
39
|
+
projectPathState: AregPathState;
|
|
40
|
+
piDir: AregPathState;
|
|
41
|
+
piSettings: AregTextFileState;
|
|
42
|
+
replacement: AregReplacementInspection;
|
|
43
|
+
skills: readonly AregSkillKindSkillInspection[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type AregInitProjectInspection = AregProjectBaseInspection & AregInstructionFilesInspection;
|
|
47
|
+
|
|
48
|
+
export async function inspectInitProject(
|
|
49
|
+
ctx: AregCliContext,
|
|
50
|
+
projectPath: string,
|
|
51
|
+
): Promise<AregInitProjectInspection> {
|
|
52
|
+
const base = await ctx.project.inspectProjectBase({ cwd: ctx.cwd, projectPath, env: ctx.env });
|
|
53
|
+
const instructionFiles = await ctx.project.inspectInstructionFiles({
|
|
54
|
+
projectDir: base.projectDir,
|
|
55
|
+
env: ctx.env,
|
|
56
|
+
});
|
|
57
|
+
return { ...base, ...instructionFiles };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function collectProjectInspectionFacts(
|
|
61
|
+
ctx: AregCliContext,
|
|
62
|
+
projectPath: string,
|
|
63
|
+
): Promise<AregProjectInspectionFacts> {
|
|
64
|
+
const base = await ctx.project.inspectProjectBase({ cwd: ctx.cwd, projectPath, env: ctx.env });
|
|
65
|
+
const piArtifacts = await ctx.project.inspectPiArtifacts({
|
|
66
|
+
projectDir: base.projectDir,
|
|
67
|
+
env: ctx.env,
|
|
68
|
+
});
|
|
69
|
+
const skillInventory = await ctx.project.inspectSkillNameInventory({
|
|
70
|
+
projectDir: base.projectDir,
|
|
71
|
+
env: ctx.env,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
piDir: piArtifacts.piDir,
|
|
76
|
+
piSettings: piArtifacts.piSettings,
|
|
77
|
+
replacement: piArtifacts.replacement,
|
|
78
|
+
skillInventory,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function inspectCheckProject(
|
|
83
|
+
ctx: AregCliContext,
|
|
84
|
+
projectPath: string,
|
|
85
|
+
): Promise<AregCheckProjectInspection> {
|
|
86
|
+
const facts = await collectProjectInspectionFacts(ctx, projectPath);
|
|
87
|
+
const excludedSkillNames = await ctx.project.readLocallyExcludedSkillNames({
|
|
88
|
+
projectDir: facts.projectDir,
|
|
89
|
+
env: ctx.env,
|
|
90
|
+
});
|
|
91
|
+
const lockfileResult = parseInspectedLockfile(facts);
|
|
92
|
+
const lockfileSkillNames = lockfileResult.ok
|
|
93
|
+
? lockfileResult.value.skills.map((skill) => skill.name)
|
|
94
|
+
: [];
|
|
95
|
+
const skillNames = uniqueSortedStrings([
|
|
96
|
+
...lockfileSkillNames,
|
|
97
|
+
...facts.skillInventory.skillsDirectoryNames,
|
|
98
|
+
...facts.skillInventory.agentsSkillNames,
|
|
99
|
+
...facts.skillInventory.claudeSkillNames,
|
|
100
|
+
]);
|
|
101
|
+
return {
|
|
102
|
+
projectDir: facts.projectDir,
|
|
103
|
+
projectPathState: facts.projectPathState,
|
|
104
|
+
lockfile: facts.lockfile,
|
|
105
|
+
skillsDirectoryNames: facts.skillInventory.skillsDirectoryNames,
|
|
106
|
+
agentsSkillNames: facts.skillInventory.agentsSkillNames,
|
|
107
|
+
excludedSkillNames,
|
|
108
|
+
piDir: facts.piDir,
|
|
109
|
+
piSettings: facts.piSettings,
|
|
110
|
+
replacement: facts.replacement,
|
|
111
|
+
skills: await collectCheckSkillInspections(ctx, facts.projectDir, skillNames),
|
|
112
|
+
pairingDirectories: await ctx.project.inspectPairingDirectories({
|
|
113
|
+
projectDir: facts.projectDir,
|
|
114
|
+
env: ctx.env,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function inspectSkillKindProject(
|
|
120
|
+
ctx: AregCliContext,
|
|
121
|
+
projectPath: string,
|
|
122
|
+
): Promise<AregSkillKindProjectInspection> {
|
|
123
|
+
const facts = await collectProjectInspectionFacts(ctx, projectPath);
|
|
124
|
+
return {
|
|
125
|
+
projectDir: facts.projectDir,
|
|
126
|
+
projectPathState: facts.projectPathState,
|
|
127
|
+
piDir: facts.piDir,
|
|
128
|
+
piSettings: facts.piSettings,
|
|
129
|
+
replacement: facts.replacement,
|
|
130
|
+
skills: await collectSkillKindInspections(
|
|
131
|
+
ctx,
|
|
132
|
+
facts.projectDir,
|
|
133
|
+
facts.skillInventory.skillKindNames,
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function collectCheckSkillInspections(
|
|
139
|
+
ctx: AregCliContext,
|
|
140
|
+
projectDir: string,
|
|
141
|
+
skillNames: readonly string[],
|
|
142
|
+
): Promise<readonly AregCheckSkillInspection[]> {
|
|
143
|
+
return await collectSkillInspections(skillNames, (skillName) =>
|
|
144
|
+
ctx.project.inspectCheckSkill({ projectDir, skillName, env: ctx.env }),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function collectSkillKindInspections(
|
|
149
|
+
ctx: AregCliContext,
|
|
150
|
+
projectDir: string,
|
|
151
|
+
skillNames: readonly string[],
|
|
152
|
+
): Promise<readonly AregSkillKindSkillInspection[]> {
|
|
153
|
+
return await collectSkillInspections(skillNames, (skillName) =>
|
|
154
|
+
ctx.project.inspectSkillKindSkill({ projectDir, skillName, env: ctx.env }),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function collectSkillInspections<T>(
|
|
159
|
+
skillNames: readonly string[],
|
|
160
|
+
inspect: (skillName: string) => Promise<T>,
|
|
161
|
+
): Promise<readonly T[]> {
|
|
162
|
+
const skills: T[] = [];
|
|
163
|
+
for (const skillName of skillNames) skills.push(await inspect(skillName));
|
|
164
|
+
return skills;
|
|
165
|
+
}
|