@pithyjs/codex 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4401 -0
  3. package/dist/annotations.d.ts +106 -0
  4. package/dist/annotations.js +306 -0
  5. package/dist/apply.d.ts +2 -0
  6. package/dist/apply.js +80 -0
  7. package/dist/changed-scope.d.ts +23 -0
  8. package/dist/changed-scope.js +31 -0
  9. package/dist/check.d.ts +2 -0
  10. package/dist/check.js +117 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/env.d.ts +5 -0
  14. package/dist/env.js +35 -0
  15. package/dist/extract-cli.d.ts +23 -0
  16. package/dist/extract-cli.js +192 -0
  17. package/dist/extraction/breaking-changes.d.ts +66 -0
  18. package/dist/extraction/breaking-changes.js +352 -0
  19. package/dist/extraction/example-extractor.d.ts +55 -0
  20. package/dist/extraction/example-extractor.js +272 -0
  21. package/dist/extraction/index.d.ts +27 -0
  22. package/dist/extraction/index.js +31 -0
  23. package/dist/extraction/jsdoc-parser.d.ts +43 -0
  24. package/dist/extraction/jsdoc-parser.js +274 -0
  25. package/dist/extraction/pipeline.d.ts +54 -0
  26. package/dist/extraction/pipeline.js +526 -0
  27. package/dist/extraction/readme-sync.d.ts +108 -0
  28. package/dist/extraction/readme-sync.js +592 -0
  29. package/dist/extraction/snapshot-store.d.ts +40 -0
  30. package/dist/extraction/snapshot-store.js +153 -0
  31. package/dist/extraction/source-linker.d.ts +80 -0
  32. package/dist/extraction/source-linker.js +316 -0
  33. package/dist/extraction/test-example-extractor.d.ts +69 -0
  34. package/dist/extraction/test-example-extractor.js +400 -0
  35. package/dist/extraction/test-pattern-extractor.d.ts +68 -0
  36. package/dist/extraction/test-pattern-extractor.js +261 -0
  37. package/dist/extraction/testing-pyramid.d.ts +44 -0
  38. package/dist/extraction/testing-pyramid.js +163 -0
  39. package/dist/extraction/type-extractor.d.ts +34 -0
  40. package/dist/extraction/type-extractor.js +494 -0
  41. package/dist/extraction/types.d.ts +401 -0
  42. package/dist/extraction/types.js +34 -0
  43. package/dist/indexer.d.ts +1 -0
  44. package/dist/indexer.js +107 -0
  45. package/dist/llm.d.ts +8 -0
  46. package/dist/llm.js +75 -0
  47. package/dist/readme-sync-cli.d.ts +20 -0
  48. package/dist/readme-sync-cli.js +167 -0
  49. package/dist/review.d.ts +2 -0
  50. package/dist/review.js +93 -0
  51. package/dist/scan.d.ts +29 -0
  52. package/dist/scan.js +221 -0
  53. package/dist/schema.d.ts +169 -0
  54. package/dist/schema.js +70 -0
  55. package/dist/snapshot-cli.d.ts +45 -0
  56. package/dist/snapshot-cli.js +217 -0
  57. package/dist/sync-cli.d.ts +22 -0
  58. package/dist/sync-cli.js +154 -0
  59. package/dist/sync-pipeline.d.ts +58 -0
  60. package/dist/sync-pipeline.js +104 -0
  61. package/dist/sync.d.ts +2 -0
  62. package/dist/sync.js +318 -0
  63. package/dist/validate-cli.d.ts +20 -0
  64. package/dist/validate-cli.js +144 -0
  65. package/dist/validate.d.ts +76 -0
  66. package/dist/validate.js +183 -0
  67. package/dist/watch-cli.d.ts +21 -0
  68. package/dist/watch-cli.js +220 -0
  69. package/package.json +62 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.types",
5
+ * "title": "Extraction Types",
6
+ * "category": "types"
7
+ * }
8
+ */
9
+ /**
10
+ * Source location information for linking docs to code
11
+ */
12
+ export interface SourceLocation {
13
+ file: string;
14
+ line: number;
15
+ column?: number;
16
+ endLine?: number;
17
+ endColumn?: number;
18
+ }
19
+ /**
20
+ * Parsed JSDoc/TSDoc parameter documentation
21
+ */
22
+ export interface ParamDoc {
23
+ name: string;
24
+ type?: string;
25
+ description: string;
26
+ optional?: boolean;
27
+ defaultValue?: string;
28
+ }
29
+ /**
30
+ * Parsed JSDoc/TSDoc return documentation
31
+ */
32
+ export interface ReturnDoc {
33
+ type?: string;
34
+ description: string;
35
+ }
36
+ /**
37
+ * Extracted example block from documentation
38
+ */
39
+ export interface ExtractedExample {
40
+ code: string;
41
+ language: string;
42
+ description?: string;
43
+ runnable: boolean;
44
+ isDoctest: boolean;
45
+ location: SourceLocation;
46
+ }
47
+ /**
48
+ * Parsed JSDoc/TSDoc block with all metadata
49
+ */
50
+ export interface ParsedJSDoc {
51
+ description: string;
52
+ params: ParamDoc[];
53
+ returns?: ReturnDoc;
54
+ examples: ExtractedExample[];
55
+ throws?: string[];
56
+ see?: string[];
57
+ since?: string;
58
+ deprecated?: string | boolean;
59
+ tags: Record<string, string[]>;
60
+ location: SourceLocation;
61
+ }
62
+ /**
63
+ * Test case extracted from test files
64
+ */
65
+ export interface ExtractedTestCase {
66
+ name: string;
67
+ description?: string;
68
+ type: 'describe' | 'it' | 'test';
69
+ location: SourceLocation;
70
+ parentDescribe?: string;
71
+ code: string;
72
+ assertions: string[];
73
+ }
74
+ /**
75
+ * Test file analysis result
76
+ */
77
+ export interface TestFileAnalysis {
78
+ file: string;
79
+ testCases: ExtractedTestCase[];
80
+ imports: string[];
81
+ coveredApis: string[];
82
+ }
83
+ /**
84
+ * Enriched API entry with extracted documentation
85
+ */
86
+ export interface EnrichedApiEntry {
87
+ id: string;
88
+ name: string;
89
+ parent: string;
90
+ signature?: string;
91
+ stability: 'internal' | 'experimental' | 'stable' | 'deprecated';
92
+ jsdoc?: ParsedJSDoc;
93
+ examples: ExtractedExample[];
94
+ testRefs: TestReference[];
95
+ sourceLocation: SourceLocation;
96
+ }
97
+ /**
98
+ * Reference to a test that covers an API
99
+ */
100
+ export interface TestReference {
101
+ file: string;
102
+ line: number;
103
+ name: string;
104
+ type: 'unit' | 'integration' | 'e2e';
105
+ }
106
+ /**
107
+ * Enriched component with all extracted data
108
+ */
109
+ export interface EnrichedComponent {
110
+ id: string;
111
+ title: string;
112
+ category: string;
113
+ risk?: RiskLevel;
114
+ testing?: TestingRequirements;
115
+ description?: string;
116
+ apis: EnrichedApiEntry[];
117
+ examples: ExtractedExample[];
118
+ testRefs: TestReference[];
119
+ sourceFiles: SourceLocation[];
120
+ readmeSections?: ReadmeSection[];
121
+ }
122
+ /**
123
+ * Section extracted from README files
124
+ */
125
+ export interface ReadmeSection {
126
+ heading: string;
127
+ level: number;
128
+ content: string;
129
+ codeBlocks: ExtractedExample[];
130
+ linkedEntryId?: string;
131
+ }
132
+ /**
133
+ * Configuration for the extraction pipeline
134
+ */
135
+ export interface ExtractionConfig {
136
+ /** Root directory to scan */
137
+ root: string;
138
+ /** Glob patterns for source files */
139
+ sourcePatterns: string[];
140
+ /** Glob patterns for test files */
141
+ testPatterns: string[];
142
+ /** Glob patterns for README files */
143
+ readmePatterns: string[];
144
+ /** Whether to extract examples as runnable */
145
+ extractRunnableExamples: boolean;
146
+ /** Whether to parse test files for coverage */
147
+ analyzeTests: boolean;
148
+ /** Whether to link README sections */
149
+ linkReadmes: boolean;
150
+ /** Optional path to vitest JSON output for test status matching */
151
+ vitestOutputPath?: string;
152
+ }
153
+ /**
154
+ * Result of the extraction pipeline
155
+ */
156
+ export interface ExtractionResult {
157
+ components: EnrichedComponent[];
158
+ testAnalysis: TestFileAnalysis[];
159
+ readmeSections: ReadmeSection[];
160
+ stats: ExtractionStats;
161
+ }
162
+ /**
163
+ * Statistics from the extraction process
164
+ */
165
+ export interface ExtractionStats {
166
+ filesScanned: number;
167
+ componentsFound: number;
168
+ apisExtracted: number;
169
+ examplesExtracted: number;
170
+ testsAnalyzed: number;
171
+ readmeSectionsLinked: number;
172
+ processingTimeMs: number;
173
+ }
174
+ /**
175
+ * Default extraction configuration (all fields except root)
176
+ */
177
+ export declare const DEFAULT_EXTRACTION_CONFIG: Omit<ExtractionConfig, 'root'>;
178
+ /**
179
+ * Extracted TypeScript type/interface definition
180
+ */
181
+ export interface ExtractedTypeDefinition {
182
+ name: string;
183
+ kind: 'interface' | 'type' | 'enum' | 'class';
184
+ exported: boolean;
185
+ generics?: string[];
186
+ extends?: string[];
187
+ implements?: string[];
188
+ members: TypeMember[];
189
+ jsdoc?: ParsedJSDoc;
190
+ location: SourceLocation;
191
+ }
192
+ /**
193
+ * Member of a type/interface/class
194
+ */
195
+ export interface TypeMember {
196
+ name: string;
197
+ type: string;
198
+ optional: boolean;
199
+ readonly: boolean;
200
+ description?: string;
201
+ signature?: string;
202
+ }
203
+ /**
204
+ * Test example extracted via @codex:example marker
205
+ */
206
+ export interface TestExample {
207
+ /** The codex entry ID this example belongs to */
208
+ entryId: string;
209
+ /** Name/description of the example */
210
+ name: string;
211
+ /** The actual test code */
212
+ code: string;
213
+ /** Source location in the test file */
214
+ location: SourceLocation;
215
+ /** Test file path */
216
+ testFile: string;
217
+ /** Whether this test is passing */
218
+ status?: 'passing' | 'failing' | 'skipped' | 'unknown';
219
+ /** Error message if failing */
220
+ error?: string;
221
+ }
222
+ /**
223
+ * Test status from vitest/jest output
224
+ */
225
+ export interface TestStatus {
226
+ file: string;
227
+ testName: string;
228
+ status: 'passed' | 'failed' | 'skipped';
229
+ duration?: number;
230
+ error?: string;
231
+ }
232
+ /**
233
+ * Extended extraction result with type definitions and test examples
234
+ */
235
+ export interface ExtendedExtractionResult extends ExtractionResult {
236
+ typeDefinitions: ExtractedTypeDefinition[];
237
+ testExamples: TestExample[];
238
+ testStatuses: TestStatus[];
239
+ }
240
+ /**
241
+ * Marker type for auto-populated README sections
242
+ */
243
+ export type ReadmeMarkerType = 'install' | 'examples' | 'api' | 'bundle' | 'testing';
244
+ /**
245
+ * Parsed README marker with its attributes and position
246
+ */
247
+ export interface ReadmeMarker {
248
+ /** Marker type: install, examples, api, bundle, testing */
249
+ type: ReadmeMarkerType;
250
+ /** Raw attributes from the marker tag */
251
+ attributes: Record<string, string>;
252
+ /** The original opening tag string (preserved for idempotent rewrites) */
253
+ openTag: string;
254
+ /** Start position (character index) in the README content */
255
+ startIndex: number;
256
+ /** End position (character index) after <!-- @codex:end --> */
257
+ endIndex: number;
258
+ /** The full original content between markers (including markers themselves) */
259
+ originalContent: string;
260
+ /** Just the content between markers (excluding marker tags) */
261
+ innerContent: string;
262
+ }
263
+ /**
264
+ * Result of syncing a single README file
265
+ */
266
+ export interface ReadmeSyncResult {
267
+ /** Path to the README file */
268
+ filePath: string;
269
+ /** Whether any content was changed */
270
+ changed: boolean;
271
+ /** Number of markers processed */
272
+ markersProcessed: number;
273
+ /** Number of markers that generated new content */
274
+ markersUpdated: number;
275
+ /** Markers that could not be processed (with reasons) */
276
+ warnings: string[];
277
+ /** The updated content (only if changed) */
278
+ updatedContent?: string;
279
+ }
280
+ /**
281
+ * Generic logger interface used by codex CLI tools and pipelines.
282
+ */
283
+ export interface CodexLogger {
284
+ log(message: string): void;
285
+ warn(message: string): void;
286
+ error(message: string): void;
287
+ }
288
+ /**
289
+ * Configuration for README sync.
290
+ * All fields are optional — `syncAllReadmes` provides sensible defaults.
291
+ */
292
+ export interface ReadmeSyncConfig {
293
+ /** Project root directory (default: cwd) */
294
+ root?: string;
295
+ /** Glob patterns for README files to sync (default: `packages/*/README.md`, `README.md`) */
296
+ readmePatterns?: string[];
297
+ /** Whether to write changes to disk or just report (default: false) */
298
+ dryRun?: boolean;
299
+ /** Whether to show verbose output (default: false) */
300
+ verbose?: boolean;
301
+ }
302
+ /**
303
+ * Individual API entry within a snapshot
304
+ */
305
+ export interface SnapshotEntry {
306
+ /** API name */
307
+ name: string;
308
+ /** Parent component/module id */
309
+ parent: string;
310
+ /** Qualified name: parent.name */
311
+ qualifiedName: string;
312
+ /** Function/type signature */
313
+ signature?: string;
314
+ /** Stability level */
315
+ stability: 'internal' | 'experimental' | 'stable' | 'deprecated';
316
+ /** Kind for type definitions (interface, type, enum, class) */
317
+ kind?: 'interface' | 'type' | 'enum' | 'class';
318
+ /** Members for type definitions */
319
+ members?: TypeMember[];
320
+ }
321
+ /**
322
+ * A point-in-time snapshot of all public APIs at a given version
323
+ */
324
+ export interface ApiSnapshot {
325
+ /** Semantic version string */
326
+ version: string;
327
+ /** ISO timestamp when the snapshot was created */
328
+ timestamp: string;
329
+ /** All API entries captured in this snapshot */
330
+ entries: SnapshotEntry[];
331
+ }
332
+ /** Kind of change detected between two API versions */
333
+ export type ChangeKind = 'added' | 'removed' | 'signature-changed' | 'stability-changed' | 'members-changed';
334
+ /**
335
+ * A single detected change between two API snapshots
336
+ */
337
+ export interface ApiChange {
338
+ /** API name */
339
+ name: string;
340
+ /** Parent component/module id */
341
+ parent: string;
342
+ /** Qualified name: parent.name */
343
+ qualifiedName: string;
344
+ /** Type of change */
345
+ changeKind: ChangeKind;
346
+ /** Entry from the old snapshot (absent for additions) */
347
+ before?: SnapshotEntry;
348
+ /** Entry from the new snapshot (absent for removals) */
349
+ after?: SnapshotEntry;
350
+ }
351
+ /**
352
+ * Result of diffing two API snapshots
353
+ */
354
+ export interface SnapshotDiff {
355
+ /** Version of the older snapshot */
356
+ fromVersion: string;
357
+ /** Version of the newer snapshot */
358
+ toVersion: string;
359
+ /** APIs that were added */
360
+ added: ApiChange[];
361
+ /** APIs that were removed */
362
+ removed: ApiChange[];
363
+ /** APIs whose signature, members, or stability changed */
364
+ changed: ApiChange[];
365
+ /** Count of APIs that remain unchanged */
366
+ unchanged: number;
367
+ }
368
+ /** Semver bump classification for a change */
369
+ export type ChangeClassification = 'major' | 'minor' | 'patch';
370
+ /** Risk level for a codex entry, affects testing requirements */
371
+ export type RiskLevel = 'critical' | 'high' | 'medium' | 'low';
372
+ /**
373
+ * Testing level requirements and status
374
+ */
375
+ export interface TestLevel {
376
+ /** Whether this test level is required for this feature */
377
+ required: boolean;
378
+ /** Whether tests currently exist */
379
+ covered?: boolean;
380
+ /** Minimum percentage coverage target */
381
+ coverage?: number;
382
+ /** Path to the test file */
383
+ file?: string;
384
+ /** Specific aspects to test */
385
+ focus?: string[];
386
+ /** Required scenarios (for e2e) */
387
+ scenarios?: string[];
388
+ /** Dependencies to integrate with (for integration tests) */
389
+ dependencies?: string[];
390
+ }
391
+ /**
392
+ * Testing requirements for a codex entry across all pyramid levels
393
+ */
394
+ export interface TestingRequirements {
395
+ /** Unit test requirements */
396
+ unit?: TestLevel;
397
+ /** Integration test requirements */
398
+ integration?: TestLevel;
399
+ /** End-to-end test requirements */
400
+ e2e?: TestLevel;
401
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.types",
5
+ * "title": "Extraction Types",
6
+ * "category": "types"
7
+ * }
8
+ */
9
+ /**
10
+ * Default extraction configuration (all fields except root)
11
+ */
12
+ export const DEFAULT_EXTRACTION_CONFIG = {
13
+ sourcePatterns: [
14
+ 'packages/*/src/**/*.ts',
15
+ 'packages/*/src/**/*.js',
16
+ 'packages/*/src/**/*.mjs',
17
+ 'apps/*/src/**/*.ts',
18
+ 'apps/*/src/**/*.js',
19
+ 'apps/*/src/**/*.mjs',
20
+ ],
21
+ testPatterns: [
22
+ 'packages/*/src/**/*.test.ts',
23
+ 'packages/*/src/**/*.spec.ts',
24
+ 'packages/*/tests/**/*.ts',
25
+ 'apps/*/src/**/*.test.ts',
26
+ 'apps/*/src/**/*.spec.ts',
27
+ 'apps/*/tests/**/*.ts',
28
+ 'e2e/tests/**/*.ts',
29
+ ],
30
+ readmePatterns: ['packages/*/README.md', 'README.md'],
31
+ extractRunnableExamples: true,
32
+ analyzeTests: true,
33
+ linkReadmes: true,
34
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.indexer",
5
+ * "title": "Index Generator",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Regenerates codex index files from entries.
10
+ */
11
+ import fs from 'node:fs/promises';
12
+ import { watch as fsWatch } from 'node:fs';
13
+ import { join, dirname, relative } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { codexSchema } from './schema.js';
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const repoRoot = join(__dirname, '../../..');
18
+ const codexDir = join(repoRoot, 'codex');
19
+ const indexPath = join(repoRoot, 'codex.index.json');
20
+ async function ensureDir(dir) {
21
+ try {
22
+ await fs.mkdir(dir, { recursive: true });
23
+ }
24
+ catch {
25
+ // ignore
26
+ }
27
+ }
28
+ async function getJsonFiles(dir) {
29
+ const dirents = await fs
30
+ .readdir(dir, { withFileTypes: true })
31
+ .catch(() => []);
32
+ const files = [];
33
+ for (const dirent of dirents) {
34
+ const res = join(dir, dirent.name);
35
+ if (dirent.isDirectory()) {
36
+ // Skip proposals directory — only index applied entries
37
+ if (dirent.name === '.proposals')
38
+ continue;
39
+ files.push(...(await getJsonFiles(res)));
40
+ }
41
+ else if (dirent.isFile() && res.endsWith('.json')) {
42
+ files.push(res);
43
+ }
44
+ }
45
+ return files;
46
+ }
47
+ async function buildIndex() {
48
+ await ensureDir(codexDir);
49
+ const files = await getJsonFiles(codexDir);
50
+ const entries = [];
51
+ for (const file of files) {
52
+ try {
53
+ const raw = await fs.readFile(file, 'utf8');
54
+ const data = JSON.parse(raw);
55
+ const parsed = codexSchema.safeParse(data);
56
+ if (!parsed.success) {
57
+ const issues = parsed.error.issues.map(i => i.message).join('; ');
58
+ console.warn(`Skipping ${file}: schema errors: ${issues}`);
59
+ continue;
60
+ }
61
+ entries.push({ ...parsed.data, path: relative(repoRoot, file) });
62
+ }
63
+ catch (err) {
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ console.warn(`Skipping ${file}: ${msg}`);
66
+ }
67
+ }
68
+ // deterministic sort
69
+ entries.sort((a, b) => (a.category || '').localeCompare(b.category || '') ||
70
+ (a.title || '').localeCompare(b.title || '') ||
71
+ a.id.localeCompare(b.id));
72
+ // quick lookup map
73
+ const byId = Object.fromEntries(entries.map(e => [e.id, e]));
74
+ const payload = {
75
+ updatedAt: new Date().toISOString(),
76
+ count: entries.length,
77
+ entries,
78
+ byId,
79
+ };
80
+ const tmp = `${indexPath}.tmp`;
81
+ await fs.writeFile(tmp, JSON.stringify(payload, null, 2));
82
+ await fs.rename(tmp, indexPath);
83
+ console.log(`Wrote ${indexPath} (${entries.length} entries)`);
84
+ }
85
+ function watch() {
86
+ let timer = null;
87
+ const debouncedBuild = () => {
88
+ if (timer)
89
+ clearTimeout(timer);
90
+ timer = setTimeout(() => {
91
+ buildIndex().catch(console.error);
92
+ }, 100);
93
+ };
94
+ fsWatch(codexDir, { recursive: false }, debouncedBuild);
95
+ // fs.watch won’t see new subdirs on some platforms — periodic rescan is a cheap fix
96
+ setInterval(debouncedBuild, 2000);
97
+ void buildIndex().catch(console.error);
98
+ }
99
+ if (process.argv.includes('--watch')) {
100
+ watch();
101
+ }
102
+ else {
103
+ buildIndex().catch(err => {
104
+ console.error(err);
105
+ process.exit(1);
106
+ });
107
+ }
package/dist/llm.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sends a prompt to the OpenAI API and returns the response text.
3
+ * @codexApi {"parent":"pithy.codex.llm","name":"askLLM","stability":"stable","signature":"(messages: { role: 'system' | 'user'; content: string }[]) => Promise<string>"}
4
+ */
5
+ export declare function askLLM(messages: {
6
+ role: 'system' | 'user';
7
+ content: string;
8
+ }[]): Promise<string>;
package/dist/llm.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.llm",
5
+ * "title": "LLM Integration",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Sends prompts to OpenAI for codex generation.
10
+ */
11
+ import OpenAI from 'openai';
12
+ import { loadEnv } from './env.js';
13
+ loadEnv();
14
+ const baseURL = process.env.OPENAI_BASE_URL || undefined;
15
+ const apiKey = process.env.OPENAI_API_KEY || 'EMPTY';
16
+ const model = process.env.CODEX_MODEL || 'gpt-5-mini';
17
+ // Optional temperature (omitted by default). We also retry if a model rejects it.
18
+ const tempEnv = process.env.CODEX_TEMPERATURE;
19
+ const parsedTemp = tempEnv !== undefined && tempEnv !== '' ? Number(tempEnv) : undefined;
20
+ const temperature = Number.isFinite(parsedTemp) ? parsedTemp : undefined;
21
+ const client = new OpenAI({ apiKey, baseURL });
22
+ function errStatus(e) {
23
+ if (e && typeof e === 'object') {
24
+ const obj = e;
25
+ return obj.status ?? obj.response?.status;
26
+ }
27
+ return undefined;
28
+ }
29
+ function errBody(e) {
30
+ if (e && typeof e === 'object') {
31
+ const obj = e;
32
+ return obj.response?.data ?? obj.message ?? String(e);
33
+ }
34
+ return String(e);
35
+ }
36
+ /**
37
+ * Sends a prompt to the OpenAI API and returns the response text.
38
+ * @codexApi {"parent":"pithy.codex.llm","name":"askLLM","stability":"stable","signature":"(messages: { role: 'system' | 'user'; content: string }[]) => Promise<string>"}
39
+ */
40
+ export async function askLLM(messages) {
41
+ // Build params and include temperature only if provided
42
+ const params = {
43
+ model,
44
+ messages,
45
+ };
46
+ if (typeof temperature === 'number')
47
+ params.temperature = temperature;
48
+ try {
49
+ const res = await client.chat.completions.create(params);
50
+ return res.choices[0]?.message?.content ?? '';
51
+ }
52
+ catch (e) {
53
+ const status = errStatus(e);
54
+ const body = errBody(e);
55
+ console.error(`[codex:llm] request failed (model="${model}", status=${status})`);
56
+ console.error(body);
57
+ // Auto-retry if the error complains about temperature
58
+ const bodyText = typeof body === 'string' ? body : JSON.stringify(body);
59
+ if (status === 400 && /temperature/i.test(bodyText)) {
60
+ try {
61
+ delete params.temperature;
62
+ console.warn('[codex:llm] retrying without temperature…');
63
+ const res = await client.chat.completions.create(params);
64
+ return res.choices[0]?.message?.content ?? '';
65
+ }
66
+ catch (e2) {
67
+ const s2 = errStatus(e2);
68
+ const b2 = errBody(e2);
69
+ console.error(`[codex:llm] retry failed (model="${model}", status=${s2})`);
70
+ console.error(b2);
71
+ }
72
+ }
73
+ return '';
74
+ }
75
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.extraction.readme-sync-cli",
6
+ * "title": "README Sync CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * CLI for syncing README files with codex extraction data.
11
+ *
12
+ * Usage:
13
+ * pnpm --filter @pithyjs/codex readme-sync [-- options]
14
+ *
15
+ * Options:
16
+ * --root <path> Project root directory (default: cwd)
17
+ * --dry-run Report changes without writing files
18
+ * --verbose Show detailed progress
19
+ */
20
+ export {};