agent-workflow-kit-cli 1.2.1 → 1.3.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.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { promises as fs } from "fs";
6
+ import path from "path";
7
+ import crypto from "crypto";
8
+ import { exec } from "child_process";
9
+ import { promisify } from "util";
10
+ const execAsync = promisify(exec);
11
+ const RUNS_DIR = ".agents/state/runs";
12
+ /**
13
+ * Validates a DirectedWorkflowGraph to guarantee it is Acyclic and structurally correct.
14
+ */
15
+ export function validateWorkflowGraph(graph) {
16
+ const nodeMap = new Map();
17
+ const adjList = new Map();
18
+ const inDegree = new Map();
19
+ for (const node of graph.nodes) {
20
+ nodeMap.set(node.id, node);
21
+ adjList.set(node.id, []);
22
+ inDegree.set(node.id, 0);
23
+ }
24
+ for (const edge of graph.edges) {
25
+ if (!nodeMap.has(edge.sourceId) || !nodeMap.has(edge.targetId)) {
26
+ throw new Error(`Edge references non-existent node: ${edge.sourceId} -> ${edge.targetId}`);
27
+ }
28
+ adjList.get(edge.sourceId).push(edge.targetId);
29
+ inDegree.set(edge.targetId, (inDegree.get(edge.targetId) || 0) + 1);
30
+ }
31
+ // 1. Cycle Detection using DFS
32
+ const visited = new Set();
33
+ const recStack = new Set();
34
+ function dfs(nodeId) {
35
+ visited.add(nodeId);
36
+ recStack.add(nodeId);
37
+ const neighbors = adjList.get(nodeId) || [];
38
+ for (const neighbor of neighbors) {
39
+ if (!visited.has(neighbor)) {
40
+ dfs(neighbor);
41
+ }
42
+ else if (recStack.has(neighbor)) {
43
+ // Exclude self-referencing retry/rollback configurations if explicit
44
+ const node = nodeMap.get(nodeId);
45
+ if (node?.type !== "retry" && node?.type !== "rollback") {
46
+ throw new Error(`Cycle detected involving node: ${nodeId} -> ${neighbor}`);
47
+ }
48
+ }
49
+ }
50
+ recStack.delete(nodeId);
51
+ }
52
+ for (const node of graph.nodes) {
53
+ if (!visited.has(node.id)) {
54
+ dfs(node.id);
55
+ }
56
+ }
57
+ // 2. Fork/Join balance validation
58
+ const forkNodes = graph.nodes.filter((n) => n.type === "fork");
59
+ const joinNodes = graph.nodes.filter((n) => n.type === "join");
60
+ if (forkNodes.length !== joinNodes.length) {
61
+ throw new Error(`Fork/Join count mismatch. Forks: ${forkNodes.length}, Joins: ${joinNodes.length}`);
62
+ }
63
+ }
64
+ /**
65
+ * Helper to evaluate condition expressions.
66
+ */
67
+ function evaluateCondition(expression, context) {
68
+ try {
69
+ // Simple sandbox execution for safe expressions
70
+ const keys = Object.keys(context);
71
+ const values = Object.values(context);
72
+ const fn = new Function(...keys, `return ${expression};`);
73
+ return !!fn(...values);
74
+ }
75
+ catch (err) {
76
+ console.warn(`Condition evaluation failed for '${expression}':`, err);
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Workflow Runtime execution engine.
82
+ */
83
+ export class WorkflowRuntime {
84
+ workspaceRoot;
85
+ runState;
86
+ constructor(workspaceRoot) {
87
+ this.workspaceRoot = workspaceRoot;
88
+ }
89
+ /**
90
+ * Initializes or resumes execution state file.
91
+ */
92
+ async initRunState(graph, initialCtx) {
93
+ const runId = "run_" + crypto.randomBytes(4).toString("hex");
94
+ this.runState = {
95
+ runId,
96
+ workflowId: graph.id,
97
+ status: "IDLE",
98
+ context: { ...initialCtx },
99
+ currentStepIndex: 0,
100
+ steps: graph.nodes.map((n) => ({
101
+ id: n.id,
102
+ name: n.name,
103
+ status: "PENDING",
104
+ })),
105
+ };
106
+ await this.persistState();
107
+ }
108
+ async persistState() {
109
+ const runDir = path.join(this.workspaceRoot, RUNS_DIR);
110
+ const runFile = path.join(runDir, `${this.runState.runId}.json`);
111
+ await fs.mkdir(runDir, { recursive: true });
112
+ await fs.writeFile(runFile, JSON.stringify(this.runState, null, 2), "utf8");
113
+ }
114
+ getRunState() {
115
+ return this.runState;
116
+ }
117
+ /**
118
+ * Runs the workflow graph execution loop.
119
+ */
120
+ async run(graph, initialContext = {}, options = {}) {
121
+ validateWorkflowGraph(graph);
122
+ await this.initRunState(graph, initialContext);
123
+ this.runState.status = "EXECUTING";
124
+ await this.persistState();
125
+ const nodeMap = new Map();
126
+ for (const node of graph.nodes) {
127
+ nodeMap.set(node.id, node);
128
+ }
129
+ const inDegree = new Map();
130
+ const parentsMap = new Map();
131
+ for (const n of graph.nodes) {
132
+ inDegree.set(n.id, 0);
133
+ parentsMap.set(n.id, []);
134
+ }
135
+ for (const edge of graph.edges) {
136
+ inDegree.set(edge.targetId, inDegree.get(edge.targetId) + 1);
137
+ parentsMap.get(edge.targetId).push(edge.sourceId);
138
+ }
139
+ // Nodes ready to execute
140
+ const queue = graph.nodes
141
+ .filter((n) => (inDegree.get(n.id) || 0) === 0)
142
+ .map((n) => n.id);
143
+ // Keep track of resolved step status
144
+ const stepStatuses = new Map();
145
+ for (const s of this.runState.steps) {
146
+ stepStatuses.set(s.id, "PENDING");
147
+ }
148
+ while (queue.length > 0) {
149
+ const nodeId = queue.shift();
150
+ const node = nodeMap.get(nodeId);
151
+ const stepIdx = this.runState.steps.findIndex((s) => s.id === nodeId);
152
+ this.runState.steps[stepIdx].status = "RUNNING";
153
+ this.runState.steps[stepIdx].startedAt = new Date().toISOString();
154
+ await this.persistState();
155
+ // Resolve role checks if configured
156
+ if (node.role) {
157
+ try {
158
+ const { loadAWOSPlugins } = await import("./registry.js");
159
+ const registry = await loadAWOSPlugins(this.workspaceRoot);
160
+ const customRole = registry.roles.get(node.role);
161
+ if (customRole) {
162
+ console.log(`[AWOS Runtime] Custom Role '${node.role}' loaded for step '${node.name}'. Asserting hooks...`);
163
+ if (customRole.hooks?.preStep) {
164
+ const { getRepositoryContext } = await import("./intelligence.js");
165
+ const { loadProfile } = await import("./profiles.js");
166
+ const repoContext = await getRepositoryContext(this.workspaceRoot);
167
+ const archProfile = await loadProfile(repoContext.architecture);
168
+ const res = await customRole.hooks.preStep({
169
+ runId: this.runState.runId,
170
+ workspaceRoot: this.workspaceRoot,
171
+ repoContext,
172
+ profile: archProfile
173
+ }, node);
174
+ if (!res.proceed) {
175
+ throw new Error(`Role preStep hook rejected execution: ${res.error || "Unknown rejection"}`);
176
+ }
177
+ }
178
+ }
179
+ else {
180
+ const { RoleRegistry } = await import("./registry.js");
181
+ const roleDef = await RoleRegistry.load(node.role, path.join(this.workspaceRoot, ".agents/roles"));
182
+ console.log(`[AWOS Runtime] Role '${roleDef.id}' loaded for step '${node.name}'. Asserting responsibilities...`);
183
+ }
184
+ }
185
+ catch (err) {
186
+ console.log(`[AWOS Runtime] Warning: Active role metadata for '${node.role}' could not be loaded: ${err instanceof Error ? err.message : String(err)}`);
187
+ }
188
+ }
189
+ try {
190
+ let success = true;
191
+ let outputs = {};
192
+ // Execute by step type
193
+ if (node.type === "task") {
194
+ const executor = node.executor || "command";
195
+ const params = node.params || {};
196
+ const { loadAWOSPlugins } = await import("./registry.js");
197
+ const registry = await loadAWOSPlugins(this.workspaceRoot);
198
+ if (registry.executors.has(executor)) {
199
+ if (options.dryRun) {
200
+ console.log(`[Dry Run] Would execute custom executor '${executor}' with params:`, params);
201
+ outputs = node.mockOutput || { log: "Custom dry run completed." };
202
+ }
203
+ else {
204
+ const executeFn = registry.executors.get(executor);
205
+ outputs = await executeFn(params, this.runState);
206
+ }
207
+ }
208
+ else if (executor === "command" && params.command) {
209
+ if (options.dryRun) {
210
+ console.log(`[Dry Run] Would execute shell command: ${params.command}`);
211
+ outputs = node.mockOutput || { log: "Dry run completed." };
212
+ }
213
+ else {
214
+ const { stdout, stderr } = await execAsync(params.command, { cwd: this.workspaceRoot });
215
+ outputs = { stdout, stderr };
216
+ }
217
+ }
218
+ else if (executor === "adr-generate") {
219
+ if (options.dryRun) {
220
+ console.log(`[Dry Run] Would generate ADR: ${params.title || "Decision"}`);
221
+ outputs = node.mockOutput || { adrId: "ADR-MOCK", status: "proposed" };
222
+ }
223
+ else {
224
+ const { ADRService } = await import("./adr.js");
225
+ const generated = await ADRService.create(this.workspaceRoot, {
226
+ title: params.title || "Decision",
227
+ status: params.status || "proposed",
228
+ context: params.context || "",
229
+ decision: params.decision || "",
230
+ consequences: params.consequences || "",
231
+ metadata: params.metadata || {},
232
+ });
233
+ outputs = { adrId: generated.id, status: generated.status };
234
+ console.log(`[AWOS Runtime] Generated Architectural Decision Record: ${generated.id}`);
235
+ }
236
+ }
237
+ }
238
+ else if (node.type === "conditional") {
239
+ const expression = node.params?.expression || "true";
240
+ const evalResult = evaluateCondition(expression, this.runState.context);
241
+ outputs = { conditionResult: evalResult };
242
+ }
243
+ else if (node.type === "approval") {
244
+ // Pause execution and ask for human approval
245
+ this.runState.steps[stepIdx].status = "WAITING_APPROVAL";
246
+ this.runState.status = "PAUSED";
247
+ await this.persistState();
248
+ console.log(`\n⚠️ Workflow paused at step '${node.name}'. Requires human approval to resume.`);
249
+ console.log(`Run 'awk run resume ${this.runState.runId}' to proceed.\n`);
250
+ return this.runState; // Suspends execution
251
+ }
252
+ if (success) {
253
+ stepStatuses.set(nodeId, "COMPLETED");
254
+ this.runState.steps[stepIdx].status = "COMPLETED";
255
+ this.runState.steps[stepIdx].outputs = outputs;
256
+ this.runState.steps[stepIdx].completedAt = new Date().toISOString();
257
+ // Merge outcomes into runtime context
258
+ this.runState.context = {
259
+ ...this.runState.context,
260
+ [nodeId]: outputs,
261
+ };
262
+ }
263
+ else {
264
+ throw new Error(`Execution failed at node '${node.name}'`);
265
+ }
266
+ }
267
+ catch (err) {
268
+ stepStatuses.set(nodeId, "FAILED");
269
+ this.runState.steps[stepIdx].status = "FAILED";
270
+ this.runState.steps[stepIdx].completedAt = new Date().toISOString();
271
+ await this.persistState();
272
+ // Handle retry and rollback
273
+ if (node.rollbackNodeId) {
274
+ console.warn(`Node failed. Triggering rollback node: ${node.rollbackNodeId}`);
275
+ queue.push(node.rollbackNodeId);
276
+ }
277
+ else {
278
+ this.runState.status = "FAILURE";
279
+ await this.persistState();
280
+ throw err;
281
+ }
282
+ }
283
+ await this.persistState();
284
+ // Find children next nodes to push to queue
285
+ const edges = graph.edges.filter((e) => e.sourceId === nodeId);
286
+ for (const edge of edges) {
287
+ const targetId = edge.targetId;
288
+ const targetNode = nodeMap.get(targetId);
289
+ // Verify if condition is met
290
+ if (node.type === "conditional" && edge.conditionExpression) {
291
+ const condPassed = evaluateCondition(edge.conditionExpression, this.runState.context);
292
+ if (!condPassed) {
293
+ stepStatuses.set(targetId, "SKIPPED");
294
+ const targetIdx = this.runState.steps.findIndex((s) => s.id === targetId);
295
+ this.runState.steps[targetIdx].status = "SKIPPED";
296
+ continue;
297
+ }
298
+ }
299
+ // Check incoming paths dependency for JOIN nodes
300
+ if (targetNode.type === "join") {
301
+ const parents = parentsMap.get(targetId) || [];
302
+ const allParentsFinished = parents.every((p) => {
303
+ const status = stepStatuses.get(p);
304
+ return status === "COMPLETED" || status === "SKIPPED" || status === "FAILED";
305
+ });
306
+ if (allParentsFinished && !queue.includes(targetId)) {
307
+ queue.push(targetId);
308
+ }
309
+ }
310
+ else {
311
+ if (!queue.includes(targetId)) {
312
+ queue.push(targetId);
313
+ }
314
+ }
315
+ }
316
+ }
317
+ const hasFailures = Array.from(stepStatuses.values()).includes("FAILED");
318
+ this.runState.status = hasFailures ? "FAILURE" : "SUCCESS";
319
+ await this.persistState();
320
+ return this.runState;
321
+ }
322
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { promises as fs } from "fs";
6
+ import path from "path";
7
+ let loadedConfig = null;
8
+ let isLoaded = false;
9
+ export async function loadConfig(cwd = process.cwd()) {
10
+ if (isLoaded) {
11
+ return loadedConfig || {};
12
+ }
13
+ const configPath = path.join(cwd, "awk.config.json");
14
+ try {
15
+ const content = await fs.readFile(configPath, "utf8");
16
+ loadedConfig = JSON.parse(content);
17
+ }
18
+ catch {
19
+ loadedConfig = {};
20
+ }
21
+ isLoaded = true;
22
+ return loadedConfig || {};
23
+ }
24
+ export function clearConfigCache() {
25
+ loadedConfig = null;
26
+ isLoaded = false;
27
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import ts from "typescript";
6
+ import { execa } from "execa";
7
+ function parseTsImports(content) {
8
+ const sourceFile = ts.createSourceFile("file.ts", content, ts.ScriptTarget.Latest, true);
9
+ const imports = [];
10
+ function visit(node) {
11
+ if (ts.isImportDeclaration(node)) {
12
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
13
+ imports.push(node.moduleSpecifier.text);
14
+ }
15
+ }
16
+ else if (ts.isExportDeclaration(node)) {
17
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
18
+ imports.push(node.moduleSpecifier.text);
19
+ }
20
+ }
21
+ else if (ts.isCallExpression(node) &&
22
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
23
+ node.arguments.length > 0) {
24
+ const arg = node.arguments[0];
25
+ if (ts.isStringLiteral(arg)) {
26
+ imports.push(arg.text);
27
+ }
28
+ }
29
+ ts.forEachChild(node, visit);
30
+ }
31
+ visit(sourceFile);
32
+ return imports;
33
+ }
34
+ function fallbackPyImports(content) {
35
+ const lines = content.split("\n");
36
+ const imports = [];
37
+ let inMultiline = false;
38
+ let multilineQuote = "";
39
+ for (let line of lines) {
40
+ line = line.trim();
41
+ if (!line)
42
+ continue;
43
+ if (!inMultiline) {
44
+ if (line.startsWith('"""')) {
45
+ inMultiline = true;
46
+ multilineQuote = '"""';
47
+ if (line.endsWith('"""') && line.length > 3) {
48
+ inMultiline = false;
49
+ }
50
+ continue;
51
+ }
52
+ if (line.startsWith("'''")) {
53
+ inMultiline = true;
54
+ multilineQuote = "'''";
55
+ if (line.endsWith("'''") && line.length > 3) {
56
+ inMultiline = false;
57
+ }
58
+ continue;
59
+ }
60
+ const hashIdx = line.indexOf("#");
61
+ if (hashIdx !== -1) {
62
+ line = line.substring(0, hashIdx).trim();
63
+ }
64
+ const importFromMatch = line.match(/^from\s+([a-zA-Z0-9._]+)\s+import/);
65
+ if (importFromMatch) {
66
+ imports.push(importFromMatch[1]);
67
+ }
68
+ else {
69
+ const importMatch = line.match(/^import\s+([a-zA-Z0-9._\s,]+)/);
70
+ if (importMatch) {
71
+ const modules = importMatch[1].split(",").map(m => m.trim());
72
+ imports.push(...modules);
73
+ }
74
+ }
75
+ }
76
+ else {
77
+ if (line.endsWith(multilineQuote)) {
78
+ inMultiline = false;
79
+ }
80
+ }
81
+ }
82
+ return imports;
83
+ }
84
+ async function parsePyImports(content) {
85
+ const pyScript = `
86
+ import ast, sys, json
87
+ try:
88
+ tree = ast.parse(sys.stdin.read())
89
+ imports = []
90
+ for node in ast.walk(tree):
91
+ if isinstance(node, ast.Import):
92
+ for name in node.names:
93
+ imports.append(name.name)
94
+ elif isinstance(node, ast.ImportFrom):
95
+ if node.module:
96
+ imports.append(node.module)
97
+ print(json.dumps(imports))
98
+ except Exception:
99
+ sys.exit(1)
100
+ `.trim();
101
+ try {
102
+ const { stdout } = await execa("python", ["-c", pyScript], {
103
+ input: content,
104
+ timeout: 2000,
105
+ });
106
+ return JSON.parse(stdout);
107
+ }
108
+ catch {
109
+ return fallbackPyImports(content);
110
+ }
111
+ }
112
+ function parseJavaImports(content) {
113
+ const cleaned = content.replace(/\/\*[\s\S]*?\*\//g, "");
114
+ const lines = cleaned.split("\n");
115
+ const imports = [];
116
+ for (let line of lines) {
117
+ line = line.trim();
118
+ const doubleSlashIdx = line.indexOf("//");
119
+ if (doubleSlashIdx !== -1) {
120
+ line = line.substring(0, doubleSlashIdx).trim();
121
+ }
122
+ if (line.startsWith("import ")) {
123
+ const match = line.match(/^import\s+(?:static\s+)?([a-zA-Z0-9._*]+)\s*;/);
124
+ if (match) {
125
+ imports.push(match[1]);
126
+ }
127
+ }
128
+ }
129
+ return imports;
130
+ }
131
+ export async function parseImports(filePath, content) {
132
+ const ext = filePath.split(".").pop()?.toLowerCase();
133
+ if (ext === "ts" || ext === "tsx") {
134
+ return parseTsImports(content);
135
+ }
136
+ if (ext === "py") {
137
+ return parsePyImports(content);
138
+ }
139
+ if (ext === "java") {
140
+ return parseJavaImports(content);
141
+ }
142
+ return [];
143
+ }
@@ -6,6 +6,7 @@ import handlebars from "handlebars";
6
6
  import { promises as fs } from "fs";
7
7
  import path from "path";
8
8
  import { fileURLToPath } from "url";
9
+ import { loadConfig } from "./config.js";
9
10
  // Register custom Handlebars helpers
10
11
  handlebars.registerHelper("eq", function (a, b) {
11
12
  return a === b;
@@ -14,13 +15,30 @@ const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
15
16
  // The templates directory is located at '../../templates' relative to 'dist/core/renderer.js'
16
17
  const TEMPLATES_DIR = path.resolve(__dirname, "../../templates");
18
+ /**
19
+ * Resolves the path of a template file, prioritizing the customTemplatesPath if configured.
20
+ */
21
+ async function resolveTemplatePath(relativePath) {
22
+ const config = await loadConfig();
23
+ if (config.customTemplatesPath) {
24
+ const customFullPath = path.resolve(process.cwd(), config.customTemplatesPath, relativePath);
25
+ try {
26
+ await fs.access(customFullPath);
27
+ return customFullPath;
28
+ }
29
+ catch {
30
+ // Fallback to default
31
+ }
32
+ }
33
+ return path.join(TEMPLATES_DIR, relativePath);
34
+ }
17
35
  /**
18
36
  * Loads a Handlebars template, compiles it, and renders it with the given context.
19
37
  * @param templatePath Relative path to the template inside the templates directory (e.g. 'common/AGENTS.md.hbs')
20
38
  * @param context Key-value context data for compilation
21
39
  */
22
40
  export async function renderTemplate(templatePath, context) {
23
- const fullPath = path.join(TEMPLATES_DIR, templatePath);
41
+ const fullPath = await resolveTemplatePath(templatePath);
24
42
  const fileContent = await fs.readFile(fullPath, "utf8");
25
43
  const compiled = handlebars.compile(fileContent);
26
44
  return compiled(context);
@@ -31,7 +49,7 @@ export async function renderTemplate(templatePath, context) {
31
49
  * @param context Optional key-value data for compilation
32
50
  */
33
51
  export async function readStaticTemplateFile(filePath, context) {
34
- const fullPath = path.join(TEMPLATES_DIR, filePath);
52
+ const fullPath = await resolveTemplatePath(filePath);
35
53
  const fileContent = await fs.readFile(fullPath, "utf8");
36
54
  if (context) {
37
55
  const compiled = handlebars.compile(fileContent);
@@ -43,38 +61,71 @@ export async function readStaticTemplateFile(filePath, context) {
43
61
  * Gets all rules files for a stack.
44
62
  */
45
63
  export async function getStackRules(stack) {
46
- const rulesDir = path.join(TEMPLATES_DIR, stack, "rules");
64
+ const relativeRulesDir = path.join(stack, "rules");
65
+ const rules = new Set();
66
+ // Try custom first
67
+ const config = await loadConfig();
68
+ if (config.customTemplatesPath) {
69
+ const customRulesDir = path.resolve(process.cwd(), config.customTemplatesPath, relativeRulesDir);
70
+ try {
71
+ const files = await fs.readdir(customRulesDir);
72
+ for (const f of files) {
73
+ if (f.endsWith(".md"))
74
+ rules.add(f);
75
+ }
76
+ }
77
+ catch {
78
+ // Ignore
79
+ }
80
+ }
81
+ // Fallback default
82
+ const defaultRulesDir = path.join(TEMPLATES_DIR, stack, "rules");
47
83
  try {
48
- const files = await fs.readdir(rulesDir);
49
- return files.filter((f) => f.endsWith(".md"));
84
+ const files = await fs.readdir(defaultRulesDir);
85
+ for (const f of files) {
86
+ if (f.endsWith(".md"))
87
+ rules.add(f);
88
+ }
50
89
  }
51
90
  catch {
52
- return [];
91
+ // Ignore
53
92
  }
93
+ return Array.from(rules);
54
94
  }
55
95
  /**
56
96
  * Gets all skills for a stack.
57
97
  */
58
98
  export async function getStackSkills(stack) {
59
- const skillsDir = path.join(TEMPLATES_DIR, stack, "skills");
60
- try {
61
- const entries = await fs.readdir(skillsDir, { withFileTypes: true });
62
- const skills = [];
63
- for (const entry of entries) {
64
- if (entry.isDirectory()) {
65
- const skillFilePath = path.join(skillsDir, entry.name, "SKILL.md");
66
- try {
67
- await fs.access(skillFilePath);
68
- skills.push(`${entry.name}/SKILL.md`);
69
- }
70
- catch {
71
- // Ignore if SKILL.md doesn't exist
99
+ const relativeSkillsDir = path.join(stack, "skills");
100
+ const skills = new Set();
101
+ const checkDir = async (dirPath) => {
102
+ try {
103
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
104
+ for (const entry of entries) {
105
+ if (entry.isDirectory()) {
106
+ const skillFilePath = path.join(dirPath, entry.name, "SKILL.md");
107
+ try {
108
+ await fs.access(skillFilePath);
109
+ skills.add(`${entry.name}/SKILL.md`);
110
+ }
111
+ catch {
112
+ // Ignore
113
+ }
72
114
  }
73
115
  }
74
116
  }
75
- return skills;
76
- }
77
- catch {
78
- return [];
117
+ catch {
118
+ // Ignore
119
+ }
120
+ };
121
+ // Check custom
122
+ const config = await loadConfig();
123
+ if (config.customTemplatesPath) {
124
+ const customSkillsDir = path.resolve(process.cwd(), config.customTemplatesPath, relativeSkillsDir);
125
+ await checkDir(customSkillsDir);
79
126
  }
127
+ // Check default
128
+ const defaultSkillsDir = path.join(TEMPLATES_DIR, stack, "skills");
129
+ await checkDir(defaultSkillsDir);
130
+ return Array.from(skills);
80
131
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "agent-workflow-kit-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "AI-Ready Repository Workflow Generator & Guideline Optimizer for Codex and Antigravity",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
8
- "agent-workflow-kit": "./dist/index.js"
8
+ "agent-workflow-kit": "./dist/index.js",
9
+ "agent-workflow-kit-cli": "./dist/index.js"
9
10
  },
10
11
  "files": [
11
12
  "dist",