@laitszkin/apollo-toolkit 3.13.2 → 3.14.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/AGENTS.md +7 -7
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +8 -8
- package/analyse-app-logs/SKILL.md +3 -3
- package/bin/apollo-toolkit.ts +7 -0
- package/codex/codex-memory-manager/SKILL.md +2 -2
- package/codex/learn-skill-from-conversations/SKILL.md +3 -3
- package/dist/bin/apollo-toolkit.d.ts +2 -0
- package/dist/bin/apollo-toolkit.js +7 -0
- package/dist/lib/cli.d.ts +41 -0
- package/dist/lib/cli.js +655 -0
- package/dist/lib/installer.d.ts +59 -0
- package/dist/lib/installer.js +404 -0
- package/dist/lib/tool-runner.d.ts +19 -0
- package/dist/lib/tool-runner.js +536 -0
- package/dist/lib/tools/architecture.d.ts +2 -0
- package/dist/lib/tools/architecture.js +23 -0
- package/dist/lib/tools/create-specs.d.ts +2 -0
- package/dist/lib/tools/create-specs.js +175 -0
- package/dist/lib/tools/docs-to-voice.d.ts +2 -0
- package/dist/lib/tools/docs-to-voice.js +705 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
- package/dist/lib/tools/extract-conversations.d.ts +2 -0
- package/dist/lib/tools/extract-conversations.js +105 -0
- package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
- package/dist/lib/tools/extract-pdf-text.js +92 -0
- package/dist/lib/tools/filter-logs.d.ts +2 -0
- package/dist/lib/tools/filter-logs.js +94 -0
- package/dist/lib/tools/find-github-issues.d.ts +2 -0
- package/dist/lib/tools/find-github-issues.js +176 -0
- package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
- package/dist/lib/tools/generate-storyboard-images.js +419 -0
- package/dist/lib/tools/log-cli-utils.d.ts +35 -0
- package/dist/lib/tools/log-cli-utils.js +233 -0
- package/dist/lib/tools/open-github-issue.d.ts +2 -0
- package/dist/lib/tools/open-github-issue.js +750 -0
- package/dist/lib/tools/read-github-issue.d.ts +2 -0
- package/dist/lib/tools/read-github-issue.js +134 -0
- package/dist/lib/tools/render-error-book.d.ts +2 -0
- package/dist/lib/tools/render-error-book.js +265 -0
- package/dist/lib/tools/render-katex.d.ts +2 -0
- package/dist/lib/tools/render-katex.js +294 -0
- package/dist/lib/tools/review-threads.d.ts +2 -0
- package/dist/lib/tools/review-threads.js +491 -0
- package/dist/lib/tools/search-logs.d.ts +2 -0
- package/dist/lib/tools/search-logs.js +164 -0
- package/dist/lib/tools/sync-memory-index.d.ts +2 -0
- package/dist/lib/tools/sync-memory-index.js +113 -0
- package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
- package/dist/lib/tools/validate-openai-agent-config.js +190 -0
- package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
- package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
- package/dist/lib/types.d.ts +82 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/updater.d.ts +34 -0
- package/dist/lib/updater.js +112 -0
- package/dist/lib/utils/format.d.ts +2 -0
- package/dist/lib/utils/format.js +6 -0
- package/dist/lib/utils/terminal.d.ts +12 -0
- package/dist/lib/utils/terminal.js +26 -0
- package/docs-to-voice/SKILL.md +0 -1
- package/generate-spec/SKILL.md +1 -1
- package/katex/SKILL.md +1 -2
- package/lib/cli.ts +780 -0
- package/lib/installer.ts +466 -0
- package/lib/tool-runner.ts +561 -0
- package/lib/tools/architecture.ts +20 -0
- package/lib/tools/create-specs.ts +204 -0
- package/lib/tools/docs-to-voice.ts +799 -0
- package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
- package/lib/tools/extract-conversations.ts +114 -0
- package/lib/tools/extract-pdf-text.ts +99 -0
- package/lib/tools/filter-logs.ts +118 -0
- package/lib/tools/find-github-issues.ts +211 -0
- package/lib/tools/generate-storyboard-images.ts +455 -0
- package/lib/tools/log-cli-utils.ts +262 -0
- package/lib/tools/open-github-issue.ts +930 -0
- package/lib/tools/read-github-issue.ts +179 -0
- package/lib/tools/render-error-book.ts +300 -0
- package/lib/tools/render-katex.ts +325 -0
- package/lib/tools/review-threads.ts +590 -0
- package/lib/tools/search-logs.ts +200 -0
- package/lib/tools/sync-memory-index.ts +114 -0
- package/lib/tools/validate-openai-agent-config.ts +213 -0
- package/lib/tools/validate-skill-frontmatter.ts +124 -0
- package/lib/types.ts +90 -0
- package/lib/updater.ts +165 -0
- package/lib/utils/format.ts +7 -0
- package/lib/utils/terminal.ts +22 -0
- package/open-github-issue/SKILL.md +2 -2
- package/optimise-skill/SKILL.md +1 -1
- package/package.json +13 -4
- package/resources/project-architecture/assets/architecture.css +764 -0
- package/resources/project-architecture/assets/viewer.client.js +144 -0
- package/resources/project-architecture/index.html +42 -0
- package/review-spec-related-changes/SKILL.md +1 -1
- package/solve-issues-found-during-review/SKILL.md +2 -1
- package/tsconfig.json +28 -0
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
- package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
- package/analyse-app-logs/scripts/search_logs.py +0 -137
- package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
- package/analyse-app-logs/tests/test_search_logs.py +0 -100
- package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
- package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
- package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
- package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
- package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
- package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
- package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
- package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
- package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
- package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
- package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
- package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
- package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/generate-spec/scripts/create-specs +0 -215
- package/generate-spec/tests/test_create_specs.py +0 -200
- package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
- package/init-project-html/scripts/architecture.js +0 -296
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/katex/scripts/render_katex.py +0 -247
- package/katex/scripts/render_katex.sh +0 -11
- package/katex/tests/test_render_katex.py +0 -174
- package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
- package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/open_github_issue.py +0 -705
- package/open-github-issue/tests/test_open_github_issue.py +0 -381
- package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
- package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/find_issues.py +0 -148
- package/read-github-issue/scripts/read_issue.py +0 -108
- package/read-github-issue/tests/test_find_issues.py +0 -127
- package/read-github-issue/tests/test_read_issue.py +0 -109
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/resolve-review-comments/scripts/review_threads.py +0 -425
- package/resolve-review-comments/tests/test_review_threads.py +0 -74
- package/scripts/validate_openai_agent_config.py +0 -209
- package/scripts/validate_skill_frontmatter.py +0 -131
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
- package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
- package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
- package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import type { ToolContext } from '../types';
|
|
3
|
+
import {
|
|
4
|
+
extractTimestamp,
|
|
5
|
+
inWindow,
|
|
6
|
+
iterInputLines,
|
|
7
|
+
parseCliTimestamp,
|
|
8
|
+
buildTimezone,
|
|
9
|
+
validateTimeWindow,
|
|
10
|
+
} from './log-cli-utils';
|
|
11
|
+
|
|
12
|
+
interface SearchLogsArgs {
|
|
13
|
+
paths: string[];
|
|
14
|
+
keyword: string[];
|
|
15
|
+
regex: string[];
|
|
16
|
+
mode: 'any' | 'all';
|
|
17
|
+
ignoreCase: boolean;
|
|
18
|
+
start: string | null;
|
|
19
|
+
end: string | null;
|
|
20
|
+
assumeTimezone: string;
|
|
21
|
+
beforeContext: number;
|
|
22
|
+
afterContext: number;
|
|
23
|
+
countOnly: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv: string[]): SearchLogsArgs {
|
|
27
|
+
const args: SearchLogsArgs = {
|
|
28
|
+
paths: [],
|
|
29
|
+
keyword: [],
|
|
30
|
+
regex: [],
|
|
31
|
+
mode: 'any',
|
|
32
|
+
ignoreCase: false,
|
|
33
|
+
start: null,
|
|
34
|
+
end: null,
|
|
35
|
+
assumeTimezone: 'UTC',
|
|
36
|
+
beforeContext: 0,
|
|
37
|
+
afterContext: 0,
|
|
38
|
+
countOnly: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < argv.length) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg === '--keyword' && i + 1 < argv.length) {
|
|
45
|
+
args.keyword.push(argv[++i]);
|
|
46
|
+
} else if (arg === '--regex' && i + 1 < argv.length) {
|
|
47
|
+
args.regex.push(argv[++i]);
|
|
48
|
+
} else if (arg === '--mode' && i + 1 < argv.length) {
|
|
49
|
+
const val = argv[++i];
|
|
50
|
+
if (val === 'any' || val === 'all') {
|
|
51
|
+
args.mode = val;
|
|
52
|
+
}
|
|
53
|
+
} else if (arg === '--ignore-case') {
|
|
54
|
+
args.ignoreCase = true;
|
|
55
|
+
} else if (arg === '--start' && i + 1 < argv.length) {
|
|
56
|
+
args.start = argv[++i];
|
|
57
|
+
} else if (arg === '--end' && i + 1 < argv.length) {
|
|
58
|
+
args.end = argv[++i];
|
|
59
|
+
} else if (arg === '--assume-timezone' && i + 1 < argv.length) {
|
|
60
|
+
args.assumeTimezone = argv[++i];
|
|
61
|
+
} else if (arg === '--before-context' && i + 1 < argv.length) {
|
|
62
|
+
args.beforeContext = parseInt(argv[++i], 10) || 0;
|
|
63
|
+
} else if (arg === '--after-context' && i + 1 < argv.length) {
|
|
64
|
+
args.afterContext = parseInt(argv[++i], 10) || 0;
|
|
65
|
+
} else if (arg === '--count-only') {
|
|
66
|
+
args.countOnly = true;
|
|
67
|
+
} else if (arg.startsWith('-')) {
|
|
68
|
+
// skip unknown flags
|
|
69
|
+
} else {
|
|
70
|
+
args.paths.push(arg);
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Matcher {
|
|
79
|
+
(line: string): boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildMatchers(args: SearchLogsArgs): Matcher[] {
|
|
83
|
+
const matchers: Matcher[] = [];
|
|
84
|
+
|
|
85
|
+
for (const keyword of args.keyword) {
|
|
86
|
+
const needle = args.ignoreCase ? keyword.toLowerCase() : keyword;
|
|
87
|
+
matchers.push((line: string) => {
|
|
88
|
+
const haystack = args.ignoreCase ? line.toLowerCase() : line;
|
|
89
|
+
return haystack.includes(needle);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const pattern of args.regex) {
|
|
94
|
+
const flags = args.ignoreCase ? 'i' : '';
|
|
95
|
+
const compiled = new RegExp(pattern, flags);
|
|
96
|
+
matchers.push((line: string) => compiled.test(line));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return matchers;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function lineMatches(
|
|
103
|
+
line: string,
|
|
104
|
+
matchers: Matcher[],
|
|
105
|
+
mode: 'any' | 'all',
|
|
106
|
+
): boolean {
|
|
107
|
+
if (matchers.length === 0) return true;
|
|
108
|
+
if (mode === 'any') {
|
|
109
|
+
return matchers.some((m) => m(line));
|
|
110
|
+
}
|
|
111
|
+
return matchers.every((m) => m(line));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function searchLogsHandler(
|
|
115
|
+
argv: string[],
|
|
116
|
+
context: ToolContext,
|
|
117
|
+
): Promise<number> {
|
|
118
|
+
const { stdout, stderr } = context;
|
|
119
|
+
const args = parseArgs(argv);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
buildTimezone(args.assumeTimezone);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
stderr.write(`Error: invalid timezone: ${args.assumeTimezone}\n`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let start: Date | null = null;
|
|
129
|
+
let end: Date | null = null;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
if (args.start) {
|
|
133
|
+
start = parseCliTimestamp(args.start, args.assumeTimezone);
|
|
134
|
+
}
|
|
135
|
+
if (args.end) {
|
|
136
|
+
end = parseCliTimestamp(args.end, args.assumeTimezone);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
stderr.write(`Error: ${(err as Error).message}\n`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!validateTimeWindow(start, end, stderr)) {
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const matchers = buildMatchers(args);
|
|
148
|
+
let matches = 0;
|
|
149
|
+
const beforeBuffer: string[] = [];
|
|
150
|
+
let afterRemaining = 0;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
for await (const line of iterInputLines(args.paths)) {
|
|
154
|
+
const timestamp = extractTimestamp(line, args.assumeTimezone);
|
|
155
|
+
|
|
156
|
+
// When time filter is active, skip lines outside the window
|
|
157
|
+
if (args.start || args.end) {
|
|
158
|
+
if (!inWindow(timestamp, start, end)) {
|
|
159
|
+
beforeBuffer.push(line);
|
|
160
|
+
if (beforeBuffer.length > args.beforeContext) {
|
|
161
|
+
beforeBuffer.shift();
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isMatch = lineMatches(line, matchers, args.mode);
|
|
168
|
+
|
|
169
|
+
if (isMatch) {
|
|
170
|
+
matches++;
|
|
171
|
+
if (!args.countOnly) {
|
|
172
|
+
// Flush before context
|
|
173
|
+
for (const ctxLine of beforeBuffer) {
|
|
174
|
+
stdout.write(ctxLine + '\n');
|
|
175
|
+
}
|
|
176
|
+
stdout.write(line + '\n');
|
|
177
|
+
}
|
|
178
|
+
afterRemaining = args.afterContext;
|
|
179
|
+
} else if (afterRemaining > 0 && !args.countOnly) {
|
|
180
|
+
stdout.write(line + '\n');
|
|
181
|
+
afterRemaining--;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Maintain before context buffer
|
|
185
|
+
beforeBuffer.push(line);
|
|
186
|
+
if (beforeBuffer.length > args.beforeContext) {
|
|
187
|
+
beforeBuffer.shift();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
stderr.write(`Error: ${(err as Error).message}\n`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (args.countOnly) {
|
|
196
|
+
stdout.write(String(matches) + '\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ToolContext } from '../types';
|
|
4
|
+
|
|
5
|
+
const START_MARKER = '<!-- codex-memory-manager:start -->';
|
|
6
|
+
const END_MARKER = '<!-- codex-memory-manager:end -->';
|
|
7
|
+
const DEFAULT_SECTION_TITLE = '## User Memory Index';
|
|
8
|
+
const DEFAULT_INSTRUCTIONS = [
|
|
9
|
+
'Before starting work, review the index below and open any relevant user preference files.',
|
|
10
|
+
'When a new preference category appears, create or update the matching memory file and refresh this index.',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function titleFromMemoryFile(filePath: string): string {
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
for (const line of content.split('\n')) {
|
|
17
|
+
const stripped = line.trim();
|
|
18
|
+
if (stripped.startsWith('# ')) {
|
|
19
|
+
return stripped.slice(2).trim() || path.basename(filePath).replace(/\.md$/, '').replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// fall through
|
|
24
|
+
}
|
|
25
|
+
return path.basename(filePath).replace(/\.md$/, '').replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function iterMemoryFiles(memoryDir: string): string[] {
|
|
29
|
+
if (!fs.existsSync(memoryDir)) return [];
|
|
30
|
+
const entries = fs.readdirSync(memoryDir);
|
|
31
|
+
return entries
|
|
32
|
+
.filter((name) => name.endsWith('.md'))
|
|
33
|
+
.map((name) => path.join(memoryDir, name))
|
|
34
|
+
.filter((p) => fs.statSync(p).isFile())
|
|
35
|
+
.sort((a, b) => path.basename(a).toLowerCase().localeCompare(path.basename(b).toLowerCase()));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderSection(memoryFiles: string[], sectionTitle: string, instructionLines: string[]): string {
|
|
39
|
+
const lines = [START_MARKER, sectionTitle.trim(), ''];
|
|
40
|
+
|
|
41
|
+
const cleaned = instructionLines.filter((line) => line && line.trim());
|
|
42
|
+
for (const line of cleaned) {
|
|
43
|
+
lines.push(line.trim());
|
|
44
|
+
}
|
|
45
|
+
if (cleaned.length) lines.push('');
|
|
46
|
+
|
|
47
|
+
if (memoryFiles.length) {
|
|
48
|
+
const entries = memoryFiles
|
|
49
|
+
.map((p) => ({ title: titleFromMemoryFile(p), resolved: path.resolve(p) }))
|
|
50
|
+
.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()) || a.resolved.localeCompare(b.resolved));
|
|
51
|
+
for (const { title, resolved } of entries) {
|
|
52
|
+
lines.push(`- [${title}](file://${resolved})`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
lines.push('- No memory files are currently indexed.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
lines.push(END_MARKER);
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function removeExistingSection(content: string): string {
|
|
63
|
+
const pattern = new RegExp(`\n*${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\n*`, 'g');
|
|
64
|
+
return content.replace(pattern, '\n\n').trimEnd();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function escapeRegex(str: string): string {
|
|
68
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function syncAgentsFile(agentsFile: string, sectionText: string): void {
|
|
72
|
+
const dir = path.dirname(agentsFile);
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
let original = '';
|
|
76
|
+
try {
|
|
77
|
+
original = fs.readFileSync(agentsFile, 'utf8');
|
|
78
|
+
} catch {
|
|
79
|
+
// file doesn't exist
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const base = removeExistingSection(original);
|
|
83
|
+
const updated = base ? `${base}\n\n${sectionText}\n` : `${sectionText}\n`;
|
|
84
|
+
fs.writeFileSync(agentsFile, updated, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function syncMemoryIndexHandler(args: string[], context: ToolContext): Promise<number> {
|
|
88
|
+
try {
|
|
89
|
+
const homeDir = process.env.HOME || '';
|
|
90
|
+
let agentsFile = path.join(homeDir, '.codex', 'AGENTS.md');
|
|
91
|
+
let memoryDir = path.join(homeDir, '.codex', 'memory');
|
|
92
|
+
let sectionTitle = DEFAULT_SECTION_TITLE;
|
|
93
|
+
let instructionLines = [...DEFAULT_INSTRUCTIONS];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < args.length; i++) {
|
|
96
|
+
if (args[i] === '--agents-file' && i + 1 < args.length) agentsFile = args[++i];
|
|
97
|
+
else if (args[i] === '--memory-dir' && i + 1 < args.length) memoryDir = args[++i];
|
|
98
|
+
else if (args[i] === '--section-title' && i + 1 < args.length) sectionTitle = args[++i];
|
|
99
|
+
else if (args[i] === '--instruction-line' && i + 1 < args.length) instructionLines.push(args[++i]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const memoryFiles = iterMemoryFiles(memoryDir);
|
|
103
|
+
const sectionText = renderSection(memoryFiles, sectionTitle, instructionLines);
|
|
104
|
+
syncAgentsFile(agentsFile, sectionText);
|
|
105
|
+
|
|
106
|
+
context.stdout?.write(`SYNCED_AGENTS_FILE=${path.resolve(agentsFile)}\n`);
|
|
107
|
+
context.stdout?.write(`MEMORY_FILES_INDEXED=${memoryFiles.length}\n`);
|
|
108
|
+
return Promise.resolve(0);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const stderr = context.stderr || process.stderr;
|
|
111
|
+
stderr.write(`Error: ${(err as Error).message}\n`);
|
|
112
|
+
return Promise.resolve(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import type { ToolContext } from '../types';
|
|
5
|
+
|
|
6
|
+
const TOP_LEVEL_ALLOWED_KEYS = new Set(['interface', 'dependencies', 'policy']);
|
|
7
|
+
const INTERFACE_REQUIRED_KEYS = new Set(['display_name', 'short_description', 'default_prompt']);
|
|
8
|
+
const INTERFACE_ALLOWED_KEYS = new Set([
|
|
9
|
+
'display_name', 'short_description', 'default_prompt',
|
|
10
|
+
'icon_small', 'icon_large', 'brand_color',
|
|
11
|
+
]);
|
|
12
|
+
const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
|
|
13
|
+
|
|
14
|
+
function repoRoot(context?: ToolContext): string {
|
|
15
|
+
if (context?.sourceRoot) return context.sourceRoot;
|
|
16
|
+
// __dirname is dist/lib/tools/; need to go up 3 levels to project root
|
|
17
|
+
const fromDirname = path.resolve(__dirname, '..', '..', '..');
|
|
18
|
+
if (fs.existsSync(path.join(fromDirname, 'package.json'))) return fromDirname;
|
|
19
|
+
return path.resolve(__dirname, '..', '..', '..');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function iterSkillDirs(root: string): string[] {
|
|
23
|
+
return fs.readdirSync(root)
|
|
24
|
+
.filter((name) => {
|
|
25
|
+
const full = path.join(root, name);
|
|
26
|
+
return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'SKILL.md'));
|
|
27
|
+
})
|
|
28
|
+
.map((name) => path.join(root, name))
|
|
29
|
+
.sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractFrontmatter(content: string): Record<string, any> {
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
if (!lines.length || lines[0].trim() !== '---') {
|
|
35
|
+
throw new Error("SKILL.md must start with YAML frontmatter delimiter '---'.");
|
|
36
|
+
}
|
|
37
|
+
for (let i = 1; i < lines.length; i++) {
|
|
38
|
+
if (lines[i].trim() === '---') {
|
|
39
|
+
const raw = lines.slice(1, i).join('\n');
|
|
40
|
+
const parsed = yaml.load(raw);
|
|
41
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
42
|
+
throw new Error('SKILL.md frontmatter must be a YAML mapping.');
|
|
43
|
+
}
|
|
44
|
+
return parsed as Record<string, any>;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error("SKILL.md frontmatter is missing the closing '---' delimiter.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function requireNonEmptyString(container: Record<string, any>, key: string, context: string, errors: string[]): void {
|
|
51
|
+
const value = container[key];
|
|
52
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
53
|
+
errors.push(`${context}: '${key}' must be a non-empty string.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateDependencies(dependencies: any, context: string, errors: string[]): void {
|
|
58
|
+
if (typeof dependencies !== 'object' || dependencies === null) {
|
|
59
|
+
errors.push(`${context}: 'dependencies' must be a mapping.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tools = dependencies['tools'];
|
|
64
|
+
if (tools === undefined) return;
|
|
65
|
+
if (!Array.isArray(tools)) {
|
|
66
|
+
errors.push(`${context}: 'dependencies.tools' must be a list.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < tools.length; i++) {
|
|
71
|
+
const itemContext = `${context}: dependencies.tools[${i}]`;
|
|
72
|
+
const item = tools[i];
|
|
73
|
+
if (typeof item !== 'object' || item === null) {
|
|
74
|
+
errors.push(`${itemContext} must be a mapping.`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
requireNonEmptyString(item, 'type', itemContext, errors);
|
|
78
|
+
requireNonEmptyString(item, 'value', itemContext, errors);
|
|
79
|
+
|
|
80
|
+
if (typeof item['type'] === 'string' && item['type'] !== 'mcp') {
|
|
81
|
+
errors.push(`${itemContext}: unsupported tool type '${item['type']}', only 'mcp' is allowed.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const optionalKey of ['description', 'transport', 'url']) {
|
|
85
|
+
const optionalValue = item[optionalKey];
|
|
86
|
+
if (optionalValue !== undefined && (typeof optionalValue !== 'string' || !optionalValue.trim())) {
|
|
87
|
+
errors.push(`${itemContext}: '${optionalKey}' must be a non-empty string when provided.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validatePolicy(policy: any, context: string, errors: string[]): void {
|
|
94
|
+
if (typeof policy !== 'object' || policy === null) {
|
|
95
|
+
errors.push(`${context}: 'policy' must be a mapping.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const allowImplicit = policy['allow_implicit_invocation'];
|
|
100
|
+
if (allowImplicit !== undefined && typeof allowImplicit !== 'boolean') {
|
|
101
|
+
errors.push(`${context}: 'policy.allow_implicit_invocation' must be a boolean when provided.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateSkill(skillDir: string): string[] {
|
|
106
|
+
const errors: string[] = [];
|
|
107
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
108
|
+
const openaiYaml = path.join(skillDir, 'agents', 'openai.yaml');
|
|
109
|
+
|
|
110
|
+
let skillFrontmatter: Record<string, any>;
|
|
111
|
+
try {
|
|
112
|
+
skillFrontmatter = extractFrontmatter(fs.readFileSync(skillMd, 'utf8'));
|
|
113
|
+
} catch (exc: any) {
|
|
114
|
+
return [`${skillMd}: unable to read skill name for validation (${exc.message}).`];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const skillName = skillFrontmatter['name'];
|
|
118
|
+
if (typeof skillName !== 'string' || !skillName.trim()) {
|
|
119
|
+
return [`${skillMd}: frontmatter 'name' must be a non-empty string.`];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(openaiYaml)) {
|
|
123
|
+
return [`${openaiYaml}: file is required for every skill.`];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let parsed: any;
|
|
127
|
+
try {
|
|
128
|
+
parsed = yaml.load(fs.readFileSync(openaiYaml, 'utf8'));
|
|
129
|
+
} catch (exc: any) {
|
|
130
|
+
return [`${openaiYaml}: invalid YAML (${exc.message}).`];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
134
|
+
return [`${openaiYaml}: top-level structure must be a YAML mapping.`];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const topLevelKeys = new Set(Object.keys(parsed));
|
|
138
|
+
const unsupportedTopKeys = [...topLevelKeys].filter((k) => !TOP_LEVEL_ALLOWED_KEYS.has(k)).sort();
|
|
139
|
+
if (unsupportedTopKeys.length) {
|
|
140
|
+
errors.push(`${openaiYaml}: unsupported top-level keys: ${unsupportedTopKeys.join(', ')}.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const iface = parsed['interface'];
|
|
144
|
+
if (typeof iface !== 'object' || iface === null) {
|
|
145
|
+
errors.push(`${openaiYaml}: 'interface' must be a mapping.`);
|
|
146
|
+
return errors;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const missingInterfaceKeys = [...INTERFACE_REQUIRED_KEYS].filter((k) => !(k in iface)).sort();
|
|
150
|
+
if (missingInterfaceKeys.length) {
|
|
151
|
+
errors.push(`${openaiYaml}: missing required interface keys: ${missingInterfaceKeys.join(', ')}.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const unsupportedInterfaceKeys = Object.keys(iface).filter((k) => !INTERFACE_ALLOWED_KEYS.has(k)).sort();
|
|
155
|
+
if (unsupportedInterfaceKeys.length) {
|
|
156
|
+
errors.push(`${openaiYaml}: unsupported interface keys: ${unsupportedInterfaceKeys.join(', ')}.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const requiredKey of [...INTERFACE_REQUIRED_KEYS].sort()) {
|
|
160
|
+
requireNonEmptyString(iface, requiredKey, `${openaiYaml}`, errors);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const defaultPrompt = iface['default_prompt'];
|
|
164
|
+
const expectedSkillRef = `$${skillName.trim()}`;
|
|
165
|
+
if (typeof defaultPrompt === 'string' && !defaultPrompt.includes(expectedSkillRef)) {
|
|
166
|
+
errors.push(`${openaiYaml}: interface.default_prompt must reference '${expectedSkillRef}'.`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const brandColor = iface['brand_color'];
|
|
170
|
+
if (brandColor !== undefined) {
|
|
171
|
+
if (typeof brandColor !== 'string' || !HEX_COLOR_PATTERN.test(brandColor)) {
|
|
172
|
+
errors.push(`${openaiYaml}: interface.brand_color must be a hex color like '#1A2B3C'.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dependencies = parsed['dependencies'];
|
|
177
|
+
if (dependencies !== undefined) {
|
|
178
|
+
validateDependencies(dependencies, `${openaiYaml}`, errors);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const policy = parsed['policy'];
|
|
182
|
+
if (policy !== undefined) {
|
|
183
|
+
validatePolicy(policy, `${openaiYaml}`, errors);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return errors;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function validateOpenaiAgentConfigHandler(args: string[], context: ToolContext): Promise<number> {
|
|
190
|
+
const root = repoRoot(context);
|
|
191
|
+
const skillDirs = iterSkillDirs(root);
|
|
192
|
+
|
|
193
|
+
if (!skillDirs.length) {
|
|
194
|
+
context.stdout?.write('No top-level skill directories found.\n');
|
|
195
|
+
return Promise.resolve(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const allErrors: string[] = [];
|
|
199
|
+
for (const dir of skillDirs) {
|
|
200
|
+
allErrors.push(...validateSkill(dir));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (allErrors.length) {
|
|
204
|
+
context.stdout?.write('agents/openai.yaml validation failed:\n');
|
|
205
|
+
for (const error of allErrors) {
|
|
206
|
+
context.stdout?.write(`- ${error}\n`);
|
|
207
|
+
}
|
|
208
|
+
return Promise.resolve(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
context.stdout?.write(`agents/openai.yaml validation passed for ${skillDirs.length} skills.\n`);
|
|
212
|
+
return Promise.resolve(0);
|
|
213
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ToolContext } from '../types';
|
|
4
|
+
|
|
5
|
+
const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6
|
+
const REQUIRED_KEYS = new Set(['name', 'description']);
|
|
7
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
8
|
+
|
|
9
|
+
function repoRoot(context?: ToolContext): string {
|
|
10
|
+
if (context?.sourceRoot) return context.sourceRoot;
|
|
11
|
+
// __dirname is dist/lib/tools/; need to go up 3 levels to project root
|
|
12
|
+
const fromDirname = path.resolve(__dirname, '..', '..', '..');
|
|
13
|
+
if (fs.existsSync(path.join(fromDirname, 'package.json'))) return fromDirname;
|
|
14
|
+
return path.resolve(__dirname, '..', '..', '..');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function iterSkillDirs(root: string): string[] {
|
|
18
|
+
return fs.readdirSync(root)
|
|
19
|
+
.filter((name) => {
|
|
20
|
+
const full = path.join(root, name);
|
|
21
|
+
return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'SKILL.md'));
|
|
22
|
+
})
|
|
23
|
+
.map((name) => path.join(root, name))
|
|
24
|
+
.sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractFrontmatter(content: string): string {
|
|
28
|
+
const lines = content.split('\n');
|
|
29
|
+
if (!lines.length || lines[0].trim() !== '---') {
|
|
30
|
+
throw new Error("SKILL.md must start with YAML frontmatter delimiter '---'.");
|
|
31
|
+
}
|
|
32
|
+
for (let i = 1; i < lines.length; i++) {
|
|
33
|
+
if (lines[i].trim() === '---') {
|
|
34
|
+
return lines.slice(1, i).join('\n');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new Error("SKILL.md frontmatter is missing the closing '---' delimiter.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateSkill(skillDir: string): string[] {
|
|
41
|
+
const errors: string[] = [];
|
|
42
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
43
|
+
|
|
44
|
+
let content: string;
|
|
45
|
+
try {
|
|
46
|
+
content = fs.readFileSync(skillMd, 'utf8');
|
|
47
|
+
} catch (exc: any) {
|
|
48
|
+
return [`${skillMd}: cannot read file (${exc.message}).`];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let frontmatterText: string;
|
|
52
|
+
try {
|
|
53
|
+
frontmatterText = extractFrontmatter(content);
|
|
54
|
+
} catch (exc: any) {
|
|
55
|
+
return [`${skillMd}: ${exc.message}`];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Simple YAML-like parsing for frontmatter (handles the common cases)
|
|
59
|
+
let frontmatter: Record<string, any> = {};
|
|
60
|
+
for (const line of frontmatterText.split('\n')) {
|
|
61
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
62
|
+
if (match) {
|
|
63
|
+
frontmatter[match[1]] = match[2].trim();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const keys = new Set(Object.keys(frontmatter));
|
|
68
|
+
const missing = [...REQUIRED_KEYS].filter((k) => !keys.has(k));
|
|
69
|
+
const extra = [...keys].filter((k) => !REQUIRED_KEYS.has(k));
|
|
70
|
+
if (missing.length) {
|
|
71
|
+
errors.push(`${skillMd}: missing required frontmatter keys: ${missing.join(', ')}.`);
|
|
72
|
+
}
|
|
73
|
+
if (extra.length) {
|
|
74
|
+
errors.push(`${skillMd}: unsupported frontmatter keys: ${extra.join(', ')}.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const name = frontmatter['name'];
|
|
78
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
79
|
+
errors.push(`${skillMd}: 'name' must be a non-empty string.`);
|
|
80
|
+
} else {
|
|
81
|
+
const normalizedName = name.trim();
|
|
82
|
+
if (!NAME_PATTERN.test(normalizedName)) {
|
|
83
|
+
errors.push(`${skillMd}: 'name' must be kebab-case (lowercase letters, digits, and hyphens).`);
|
|
84
|
+
}
|
|
85
|
+
if (normalizedName !== path.basename(skillDir)) {
|
|
86
|
+
errors.push(`${skillMd}: frontmatter name '${normalizedName}' must match folder name '${path.basename(skillDir)}'.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const description = frontmatter['description'];
|
|
91
|
+
if (typeof description !== 'string' || !description.trim()) {
|
|
92
|
+
errors.push(`${skillMd}: 'description' must be a non-empty string.`);
|
|
93
|
+
} else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
94
|
+
errors.push(`${skillMd}: invalid description: exceeds maximum length of ${MAX_DESCRIPTION_LENGTH} characters`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function validateSkillFrontmatterHandler(args: string[], context: ToolContext): Promise<number> {
|
|
101
|
+
const root = repoRoot(context);
|
|
102
|
+
const skillDirs = iterSkillDirs(root);
|
|
103
|
+
|
|
104
|
+
if (!skillDirs.length) {
|
|
105
|
+
context.stdout?.write('No top-level skill directories found.\n');
|
|
106
|
+
return Promise.resolve(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const allErrors: string[] = [];
|
|
110
|
+
for (const dir of skillDirs) {
|
|
111
|
+
allErrors.push(...validateSkill(dir));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (allErrors.length) {
|
|
115
|
+
context.stdout?.write('SKILL.md frontmatter validation failed:\n');
|
|
116
|
+
for (const error of allErrors) {
|
|
117
|
+
context.stdout?.write(`- ${error}\n`);
|
|
118
|
+
}
|
|
119
|
+
return Promise.resolve(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
context.stdout?.write(`SKILL.md frontmatter validation passed for ${skillDirs.length} skills.\n`);
|
|
123
|
+
return Promise.resolve(0);
|
|
124
|
+
}
|