@nseng-ai/areg 0.1.1 → 0.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/package.json +7 -6
- package/src/cli.ts +0 -33
- package/src/context.ts +3 -29
- package/src/fake-gateways.ts +108 -380
- package/src/gateways/errors.ts +2 -2
- package/src/gateways/fs-utils.ts +2 -2
- package/src/gateways/project-fs.ts +29 -41
- package/src/gateways/project-gateway.ts +98 -31
- package/src/gateways.ts +34 -150
- package/src/index.ts +2 -28
- package/src/operations/check.ts +33 -20
- package/src/operations/doctor-skills.ts +71 -7
- package/src/operations/file-state.ts +4 -4
- package/src/operations/manifest-source-findings.ts +45 -0
- package/src/operations/manifest-sources.ts +119 -0
- package/src/operations/pi-settings.ts +2 -5
- package/src/operations/project-inspection.ts +32 -28
- package/src/operations/project-mutations.ts +5 -21
- package/src/operations/project-resolution.ts +2 -2
- package/src/operations/skill-find.ts +23 -3
- package/src/operations/skill-kind-apply-plan.ts +2 -2
- package/src/operations/skill-kind-frontmatter.ts +1 -1
- package/src/operations/skill-kind-inference.ts +30 -14
- package/src/operations/skill-kind.ts +34 -12
- package/src/sort.ts +3 -3
- package/src/gateways/command-constants.ts +0 -1
- package/src/gateways/github-gateway.ts +0 -111
- package/src/gateways/host-gateway.ts +0 -35
- package/src/gateways/mutation-policy.ts +0 -94
- package/src/gateways/npx-skills-gateway.ts +0 -53
- package/src/gateways/prompt-gateway.ts +0 -21
- package/src/gateways/skillx-workspace-gateway.ts +0 -220
- package/src/operations/frontmatter.ts +0 -151
- package/src/operations/init.ts +0 -548
- package/src/operations/lockfile.ts +0 -107
- package/src/operations/managed-markdown-block.ts +0 -47
- package/src/operations/project-agents.ts +0 -115
- package/src/operations/skill-mirror-conventions.ts +0 -126
- package/src/operations/skillx.ts +0 -305
- package/src/operations/toml-section.ts +0 -53
- package/src/operations/update-skills.ts +0 -325
- package/src/real-gateways.ts +0 -6
- package/src/skill-lookup.ts +0 -254
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { lstat, mkdtemp, readdir, realpath, rm } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
|
|
6
|
-
import { resultErr, resultOk } from "@nseng-ai/foundation/result";
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
AregErrorInfo,
|
|
10
|
-
AregNpxSkillsGateway,
|
|
11
|
-
AregOperationResult,
|
|
12
|
-
AregSkillxInstallRequest,
|
|
13
|
-
AregSkillxInstallResult,
|
|
14
|
-
AregSkillxInstalledSkill,
|
|
15
|
-
AregSkillxWorkspaceGateway,
|
|
16
|
-
} from "../gateways.ts";
|
|
17
|
-
import { sortStrings } from "../sort.ts";
|
|
18
|
-
import { errorInfo } from "./errors.ts";
|
|
19
|
-
import { inspectPath, isNodeErrorCode, isPathAtOrBelow } from "./fs-utils.ts";
|
|
20
|
-
|
|
21
|
-
export class RealAregSkillxWorkspaceGateway implements AregSkillxWorkspaceGateway {
|
|
22
|
-
private readonly npxSkills: AregNpxSkillsGateway;
|
|
23
|
-
|
|
24
|
-
constructor(options: { npxSkills: AregNpxSkillsGateway }) {
|
|
25
|
-
this.npxSkills = options.npxSkills;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async installIntoWorkspace(request: AregSkillxInstallRequest): Promise<AregSkillxInstallResult> {
|
|
29
|
-
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), "skillx."));
|
|
30
|
-
const install = await this.npxSkills.addSkills({
|
|
31
|
-
sourceRepo: request.sourceRepo,
|
|
32
|
-
skillNames: request.skillName === undefined ? [] : [request.skillName],
|
|
33
|
-
targetAgents: ["codex"],
|
|
34
|
-
cwd: workspaceRoot,
|
|
35
|
-
env: request.env,
|
|
36
|
-
});
|
|
37
|
-
if (install.type === "error") {
|
|
38
|
-
await removeWorkspaceQuietly(workspaceRoot);
|
|
39
|
-
return {
|
|
40
|
-
type: "error",
|
|
41
|
-
error: errorInfo(
|
|
42
|
-
"skillx-install-failed",
|
|
43
|
-
`npx skills add failed: ${install.error.message}`,
|
|
44
|
-
install.error.displayCommand,
|
|
45
|
-
),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const inspected = await inspectInstalledSkills(workspaceRoot, request.skillName);
|
|
49
|
-
if (inspected.type === "error") {
|
|
50
|
-
await removeWorkspaceQuietly(workspaceRoot);
|
|
51
|
-
return { type: "error", error: inspected.error };
|
|
52
|
-
}
|
|
53
|
-
return { type: "ok", workspace: { workspaceRoot, installedSkills: inspected.installedSkills } };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async cleanupWorkspace(request: { workspaceRoot: string }): Promise<AregOperationResult> {
|
|
57
|
-
return await cleanupSkillxWorkspace(request.workspaceRoot);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function inspectInstalledSkills(
|
|
62
|
-
workspaceRoot: string,
|
|
63
|
-
requestedSkillName: string | undefined,
|
|
64
|
-
): Promise<
|
|
65
|
-
| { type: "ok"; installedSkills: AregSkillxInstalledSkill[] }
|
|
66
|
-
| { type: "error"; error: AregErrorInfo }
|
|
67
|
-
> {
|
|
68
|
-
const skillsRoot = path.join(workspaceRoot, ".agents", "skills");
|
|
69
|
-
const skillsRootState = await inspectPath(skillsRoot);
|
|
70
|
-
if (skillsRootState.type !== "directory")
|
|
71
|
-
return { type: "error", error: errorInfo("skillx-no-skills", "No skills were installed") };
|
|
72
|
-
if (requestedSkillName !== undefined) {
|
|
73
|
-
const inspected = await inspectOneSkill(skillsRoot, requestedSkillName);
|
|
74
|
-
if (inspected.type === "error") return inspected;
|
|
75
|
-
return { type: "ok", installedSkills: [inspected.skill] };
|
|
76
|
-
}
|
|
77
|
-
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
78
|
-
const skillNames = sortStrings(
|
|
79
|
-
entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
|
|
80
|
-
);
|
|
81
|
-
if (skillNames.length === 0)
|
|
82
|
-
return { type: "error", error: errorInfo("skillx-no-skills", "No skills were installed") };
|
|
83
|
-
const installedSkills: AregSkillxInstalledSkill[] = [];
|
|
84
|
-
for (const skillName of skillNames) {
|
|
85
|
-
const inspected = await inspectOneSkill(skillsRoot, skillName);
|
|
86
|
-
if (inspected.type === "error") return inspected;
|
|
87
|
-
installedSkills.push(inspected.skill);
|
|
88
|
-
}
|
|
89
|
-
return { type: "ok", installedSkills };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function inspectOneSkill(
|
|
93
|
-
skillsRoot: string,
|
|
94
|
-
skillName: string,
|
|
95
|
-
): Promise<
|
|
96
|
-
{ type: "ok"; skill: AregSkillxInstalledSkill } | { type: "error"; error: AregErrorInfo }
|
|
97
|
-
> {
|
|
98
|
-
const directory = path.join(skillsRoot, skillName);
|
|
99
|
-
const directoryKind = await inspectPath(directory);
|
|
100
|
-
if (directoryKind.type !== "directory")
|
|
101
|
-
return {
|
|
102
|
-
type: "error",
|
|
103
|
-
error: errorInfo(
|
|
104
|
-
"skillx-skill-missing",
|
|
105
|
-
`Skill '${skillName}' was not found in installed skills`,
|
|
106
|
-
),
|
|
107
|
-
};
|
|
108
|
-
const skillFile = path.join(directory, "SKILL.md");
|
|
109
|
-
const fileKind = await inspectPath(skillFile);
|
|
110
|
-
if (fileKind.type !== "file")
|
|
111
|
-
return {
|
|
112
|
-
type: "error",
|
|
113
|
-
error: errorInfo(
|
|
114
|
-
"skillx-skill-malformed",
|
|
115
|
-
`Installed skill '${skillName}' is missing SKILL.md`,
|
|
116
|
-
),
|
|
117
|
-
};
|
|
118
|
-
return {
|
|
119
|
-
type: "ok",
|
|
120
|
-
skill: {
|
|
121
|
-
name: skillName,
|
|
122
|
-
directory,
|
|
123
|
-
skillFile,
|
|
124
|
-
relativeFiles: await listRelativeFiles(directory),
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function listRelativeFiles(root: string): Promise<string[]> {
|
|
130
|
-
const files: string[] = [];
|
|
131
|
-
async function visit(directory: string, prefix: string): Promise<void> {
|
|
132
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
133
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
134
|
-
for (const entry of entries) {
|
|
135
|
-
const relativePath = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
136
|
-
const fullPath = path.join(directory, entry.name);
|
|
137
|
-
if (entry.isDirectory()) {
|
|
138
|
-
await visit(fullPath, relativePath);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (entry.isFile()) files.push(relativePath);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
await visit(root, "");
|
|
145
|
-
return sortStrings(files);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function cleanupSkillxWorkspace(workspaceRoot: string): Promise<AregOperationResult> {
|
|
149
|
-
if (!path.basename(workspaceRoot).startsWith("skillx.")) {
|
|
150
|
-
return resultErr(
|
|
151
|
-
errorInfo(
|
|
152
|
-
"skillx-cleanup-refused",
|
|
153
|
-
`Refusing to remove non-skillx workspace: ${workspaceRoot}`,
|
|
154
|
-
),
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
let info;
|
|
158
|
-
try {
|
|
159
|
-
info = await lstat(workspaceRoot);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
if (isNodeErrorCode(error, "ENOENT"))
|
|
162
|
-
return resultErr(
|
|
163
|
-
errorInfo("skillx-cleanup-missing", `Workspace does not exist: ${workspaceRoot}`),
|
|
164
|
-
);
|
|
165
|
-
return resultErr(
|
|
166
|
-
errorInfo(
|
|
167
|
-
"skillx-cleanup-stat-failed",
|
|
168
|
-
`Could not inspect workspace: ${formatErrorMessage(error)}`,
|
|
169
|
-
),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
if (info.isSymbolicLink())
|
|
173
|
-
return resultErr(
|
|
174
|
-
errorInfo("skillx-cleanup-symlink", `Refusing to remove symlink workspace: ${workspaceRoot}`),
|
|
175
|
-
);
|
|
176
|
-
if (!info.isDirectory())
|
|
177
|
-
return resultErr(
|
|
178
|
-
errorInfo("skillx-cleanup-not-directory", `Workspace is not a directory: ${workspaceRoot}`),
|
|
179
|
-
);
|
|
180
|
-
let resolvedWorkspace: string;
|
|
181
|
-
let resolvedTemp: string;
|
|
182
|
-
try {
|
|
183
|
-
resolvedWorkspace = await realpath(workspaceRoot);
|
|
184
|
-
resolvedTemp = await realpath(os.tmpdir());
|
|
185
|
-
} catch (error) {
|
|
186
|
-
return resultErr(
|
|
187
|
-
errorInfo(
|
|
188
|
-
"skillx-cleanup-realpath-failed",
|
|
189
|
-
`Could not resolve workspace path: ${formatErrorMessage(error)}`,
|
|
190
|
-
),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
if (!isPathAtOrBelow(resolvedWorkspace, resolvedTemp)) {
|
|
194
|
-
return resultErr(
|
|
195
|
-
errorInfo(
|
|
196
|
-
"skillx-cleanup-outside-temp",
|
|
197
|
-
`Refusing to remove workspace outside temp directory: ${workspaceRoot}`,
|
|
198
|
-
),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
await rm(resolvedWorkspace, { recursive: true });
|
|
203
|
-
return resultOk(undefined);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
return resultErr(
|
|
206
|
-
errorInfo(
|
|
207
|
-
"skillx-cleanup-remove-failed",
|
|
208
|
-
`Could not remove workspace: ${formatErrorMessage(error)}`,
|
|
209
|
-
),
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function removeWorkspaceQuietly(workspaceRoot: string): Promise<void> {
|
|
215
|
-
try {
|
|
216
|
-
await rm(workspaceRoot, { recursive: true, force: true });
|
|
217
|
-
} catch {
|
|
218
|
-
// Best-effort cleanup of a directory this gateway just created; the command result carries the original failure.
|
|
219
|
-
}
|
|
220
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
splitMarkdownFrontmatter,
|
|
3
|
-
stripLineEnding,
|
|
4
|
-
} from "@nseng-ai/foundation/markdown-frontmatter";
|
|
5
|
-
import { err, type Result } from "@nseng-ai/foundation/result";
|
|
6
|
-
|
|
7
|
-
const FRONTMATTER_KEY_RE = /^(?<key>[A-Za-z0-9_-]+):(?<value>.*)$/u;
|
|
8
|
-
|
|
9
|
-
export interface SkillFrontmatterData {
|
|
10
|
-
fields: Readonly<Record<string, string>>;
|
|
11
|
-
keys: ReadonlySet<string>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type SkillFrontmatterParseResult = Result<SkillFrontmatterData>;
|
|
15
|
-
|
|
16
|
-
export type SkillFrontmatterTopLevelLineParseResult =
|
|
17
|
-
| { type: "key"; key: string; value: string }
|
|
18
|
-
| { type: "not_top_level" }
|
|
19
|
-
| { type: "invalid"; line: string };
|
|
20
|
-
|
|
21
|
-
export function parseSkillFrontmatterBlock(text: string): SkillFrontmatterParseResult {
|
|
22
|
-
const split = splitMarkdownFrontmatter(text);
|
|
23
|
-
if (split.type === "not_found")
|
|
24
|
-
return err({
|
|
25
|
-
code: "frontmatter_missing_opening_delimiter",
|
|
26
|
-
message: "missing opening frontmatter delimiter '---'",
|
|
27
|
-
});
|
|
28
|
-
if (split.type === "missing_closing_fence")
|
|
29
|
-
return err({
|
|
30
|
-
code: "frontmatter_missing_closing_delimiter",
|
|
31
|
-
message: "missing closing frontmatter delimiter '---'",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const lines = split.block.frontmatterLinesWithEndings.map(stripLineEnding);
|
|
35
|
-
const fields: Record<string, string> = {};
|
|
36
|
-
const keys = new Set<string>();
|
|
37
|
-
let currentKey: string | undefined;
|
|
38
|
-
let currentValues: string[] = [];
|
|
39
|
-
function flushCurrent(): void {
|
|
40
|
-
if (currentKey === undefined) return;
|
|
41
|
-
let rawValue = currentValues
|
|
42
|
-
.filter((value) => value.length > 0)
|
|
43
|
-
.join(" ")
|
|
44
|
-
.trim();
|
|
45
|
-
if (
|
|
46
|
-
rawValue.length >= 2 &&
|
|
47
|
-
rawValue[0] === rawValue.at(-1) &&
|
|
48
|
-
(rawValue[0] === '"' || rawValue[0] === "'")
|
|
49
|
-
)
|
|
50
|
-
rawValue = rawValue.slice(1, -1);
|
|
51
|
-
fields[currentKey] = rawValue;
|
|
52
|
-
}
|
|
53
|
-
for (const line of lines) {
|
|
54
|
-
const stripped = line.trim();
|
|
55
|
-
if (stripped.length === 0) continue;
|
|
56
|
-
if (stripped.startsWith("#")) continue;
|
|
57
|
-
const parsedLine = parseSkillFrontmatterTopLevelLine(line);
|
|
58
|
-
if (parsedLine.type === "key") {
|
|
59
|
-
flushCurrent();
|
|
60
|
-
if (keys.has(parsedLine.key))
|
|
61
|
-
return err({
|
|
62
|
-
code: "frontmatter_duplicate_key",
|
|
63
|
-
message: `duplicate frontmatter key: ${JSON.stringify(parsedLine.key)}`,
|
|
64
|
-
});
|
|
65
|
-
currentKey = parsedLine.key;
|
|
66
|
-
keys.add(currentKey);
|
|
67
|
-
currentValues = [];
|
|
68
|
-
const inlineValue = parsedLine.value.trim();
|
|
69
|
-
if (inlineValue.length > 0) currentValues.push(inlineValue);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (parsedLine.type === "invalid")
|
|
73
|
-
return err({
|
|
74
|
-
code: "frontmatter_invalid_line",
|
|
75
|
-
message: `invalid frontmatter line: ${JSON.stringify(parsedLine.line)}`,
|
|
76
|
-
});
|
|
77
|
-
if (currentKey === undefined)
|
|
78
|
-
return err({
|
|
79
|
-
code: "frontmatter_invalid_line",
|
|
80
|
-
message: `invalid frontmatter line: ${JSON.stringify(line)}`,
|
|
81
|
-
});
|
|
82
|
-
currentValues.push(line.trim());
|
|
83
|
-
}
|
|
84
|
-
flushCurrent();
|
|
85
|
-
return { ok: true, value: { fields, keys } };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function parseSkillFrontmatterTopLevelLine(
|
|
89
|
-
line: string,
|
|
90
|
-
): SkillFrontmatterTopLevelLineParseResult {
|
|
91
|
-
const content = stripLineEnding(line);
|
|
92
|
-
if (content.trim().length === 0) return { type: "not_top_level" };
|
|
93
|
-
if (content.trim().startsWith("#")) return { type: "not_top_level" };
|
|
94
|
-
if (content.startsWith(" ") || content.startsWith("\t")) return { type: "not_top_level" };
|
|
95
|
-
const match = FRONTMATTER_KEY_RE.exec(content);
|
|
96
|
-
if (match?.groups === undefined) return { type: "invalid", line: content };
|
|
97
|
-
return { type: "key", key: match.groups.key ?? "", value: match.groups.value ?? "" };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function isSkillFrontmatterTopLevelKey(line: string, key: string): boolean {
|
|
101
|
-
const parsed = parseSkillFrontmatterTopLevelLine(line);
|
|
102
|
-
return parsed.type === "key" && parsed.key === key;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function transformSkillFrontmatter(
|
|
106
|
-
text: string,
|
|
107
|
-
pathLabel: string,
|
|
108
|
-
desired: Readonly<Record<string, string | undefined>>,
|
|
109
|
-
): Result<string> {
|
|
110
|
-
const parsed = parseSkillFrontmatterBlock(text);
|
|
111
|
-
if (!parsed.ok)
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
error: { ...parsed.error, message: `${pathLabel} ${parsed.error.message}` },
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const split = splitMarkdownFrontmatter(text);
|
|
118
|
-
if (split.type !== "found")
|
|
119
|
-
return err({
|
|
120
|
-
code: "frontmatter_invalid_bounds",
|
|
121
|
-
message: `${pathLabel} invalid frontmatter bounds`,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const lines = [...split.block.linesWithEndings];
|
|
125
|
-
const nameIndex = lines.findIndex(
|
|
126
|
-
(line, index) =>
|
|
127
|
-
index > 0 && index < split.block.closingIndex && isSkillFrontmatterTopLevelKey(line, "name"),
|
|
128
|
-
);
|
|
129
|
-
if (nameIndex === -1)
|
|
130
|
-
return err({
|
|
131
|
-
code: "frontmatter_missing_name",
|
|
132
|
-
message: `${pathLabel} missing name field in frontmatter`,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const managedKeys = Object.keys(desired);
|
|
136
|
-
const kept = lines.filter(
|
|
137
|
-
(line, index) =>
|
|
138
|
-
index <= 0 ||
|
|
139
|
-
index >= split.block.closingIndex ||
|
|
140
|
-
!managedKeys.some((key) => isSkillFrontmatterTopLevelKey(line, key)),
|
|
141
|
-
);
|
|
142
|
-
const keptNameIndex = kept.findIndex(
|
|
143
|
-
(line, index) => index > 0 && isSkillFrontmatterTopLevelKey(line, "name"),
|
|
144
|
-
);
|
|
145
|
-
const additions = managedKeys.flatMap((key) => {
|
|
146
|
-
const value = desired[key];
|
|
147
|
-
return value === undefined ? [] : [`${key}: ${value}${split.block.lineEnding}`];
|
|
148
|
-
});
|
|
149
|
-
kept.splice(keptNameIndex + 1, 0, ...additions);
|
|
150
|
-
return { ok: true, value: kept.join("") };
|
|
151
|
-
}
|