coaiajs 0.1.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.
- package/CLAUDE.md +56 -0
- package/KINSHIP.md +63 -0
- package/README.md +148 -0
- package/articles/academic/creative-orientation-vs-problem-solving.md +177 -0
- package/articles/academic/jsonl-knowledge-graphs-agent-memory.md +142 -0
- package/articles/academic/langfuse-observability-llm-pipelines.md +144 -0
- package/articles/academic/medicine-wheel-software-architecture.md +163 -0
- package/articles/academic/mmot-autonomous-agents.md +156 -0
- package/articles/academic/model-context-protocol-interagent.md +161 -0
- package/articles/academic/pde-prompt-decomposition.md +186 -0
- package/articles/academic/structural-tension-in-ai-agents.md +134 -0
- package/articles/reviews/mcp-protocol-design-review.md +170 -0
- package/articles/reviews/observability-ai-systems-review.md +176 -0
- package/articles/reviews/prompt-engineering-decomposition-review.md +184 -0
- package/articles/surveys/agent-orchestration-survey.md +186 -0
- package/articles/surveys/knowledge-graph-storage-survey.md +204 -0
- package/articles/surveys/structural-tension-methodology-survey.md +154 -0
- package/articles/technical/aws-sdk-v3-polly.md +270 -0
- package/articles/technical/commander-cli-framework.md +262 -0
- package/articles/technical/dotenv-config-patterns.md +360 -0
- package/articles/technical/ioredis-vs-redis.md +142 -0
- package/articles/technical/langfuse-js-sdk-vs-rest.md +191 -0
- package/articles/technical/mcp-sdk-typescript.md +291 -0
- package/articles/technical/octokit-github-api.md +293 -0
- package/articles/technical/openai-sdk-modern.md +231 -0
- package/articles/technical/yaml-parsing-node.md +266 -0
- package/articles/technical/zod-runtime-validation.md +212 -0
- package/dist/mcp/config.d.ts +14 -0
- package/dist/mcp/config.js +185 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +324 -0
- package/dist/mcp/tools/coaiapy-tools.d.ts +8 -0
- package/dist/mcp/tools/coaiapy-tools.js +326 -0
- package/dist/mcp/tools/index.d.ts +3 -0
- package/dist/mcp/tools/index.js +3 -0
- package/dist/src/audio.d.ts +12 -0
- package/dist/src/audio.js +57 -0
- package/dist/src/cli-helpers.d.ts +9 -0
- package/dist/src/cli-helpers.js +78 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.js +1086 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +145 -0
- package/dist/src/environment.d.ts +44 -0
- package/dist/src/environment.js +146 -0
- package/dist/src/github.d.ts +47 -0
- package/dist/src/github.js +79 -0
- package/dist/src/langfuse/client.d.ts +30 -0
- package/dist/src/langfuse/client.js +75 -0
- package/dist/src/langfuse/comments.d.ts +16 -0
- package/dist/src/langfuse/comments.js +36 -0
- package/dist/src/langfuse/datasets.d.ts +19 -0
- package/dist/src/langfuse/datasets.js +140 -0
- package/dist/src/langfuse/index.d.ts +13 -0
- package/dist/src/langfuse/index.js +10 -0
- package/dist/src/langfuse/media.d.ts +11 -0
- package/dist/src/langfuse/media.js +167 -0
- package/dist/src/langfuse/observations.d.ts +18 -0
- package/dist/src/langfuse/observations.js +113 -0
- package/dist/src/langfuse/prompts.d.ts +14 -0
- package/dist/src/langfuse/prompts.js +127 -0
- package/dist/src/langfuse/scores.d.ts +54 -0
- package/dist/src/langfuse/scores.js +366 -0
- package/dist/src/langfuse/traces.d.ts +29 -0
- package/dist/src/langfuse/traces.js +225 -0
- package/dist/src/llm.d.ts +23 -0
- package/dist/src/llm.js +76 -0
- package/dist/src/narrative/graph-manager.d.ts +106 -0
- package/dist/src/narrative/graph-manager.js +915 -0
- package/dist/src/narrative/index.d.ts +9 -0
- package/dist/src/narrative/index.js +7 -0
- package/dist/src/narrative/markdown-export.d.ts +21 -0
- package/dist/src/narrative/markdown-export.js +383 -0
- package/dist/src/narrative/tool-definitions.d.ts +21 -0
- package/dist/src/narrative/tool-definitions.js +588 -0
- package/dist/src/narrative/tool-handlers.d.ts +11 -0
- package/dist/src/narrative/tool-handlers.js +462 -0
- package/dist/src/narrative/types.d.ts +2 -0
- package/dist/src/narrative/types.js +2 -0
- package/dist/src/narrative/validation.d.ts +29 -0
- package/dist/src/narrative/validation.js +144 -0
- package/dist/src/pde/index.d.ts +8 -0
- package/dist/src/pde/index.js +8 -0
- package/dist/src/pde/mcp-handlers.d.ts +7 -0
- package/dist/src/pde/mcp-handlers.js +300 -0
- package/dist/src/pde/mcp-tools.d.ts +15 -0
- package/dist/src/pde/mcp-tools.js +191 -0
- package/dist/src/pde/session-manager.d.ts +32 -0
- package/dist/src/pde/session-manager.js +205 -0
- package/dist/src/pde/stc-mapper.d.ts +26 -0
- package/dist/src/pde/stc-mapper.js +234 -0
- package/dist/src/pipeline/index.d.ts +2 -0
- package/dist/src/pipeline/index.js +3 -0
- package/dist/src/pipeline/template-engine.d.ts +20 -0
- package/dist/src/pipeline/template-engine.js +361 -0
- package/dist/src/planning/index.d.ts +7 -0
- package/dist/src/planning/index.js +7 -0
- package/dist/src/planning/mcp-handlers.d.ts +7 -0
- package/dist/src/planning/mcp-handlers.js +310 -0
- package/dist/src/planning/mcp-tools.d.ts +15 -0
- package/dist/src/planning/mcp-tools.js +145 -0
- package/dist/src/planning/plan-parser.d.ts +35 -0
- package/dist/src/planning/plan-parser.js +506 -0
- package/dist/src/redis.d.ts +21 -0
- package/dist/src/redis.js +78 -0
- package/dist/src/types.d.ts +244 -0
- package/dist/src/types.js +8 -0
- package/mcp/config.ts +196 -0
- package/mcp/server.ts +402 -0
- package/mcp/tools/coaiapy-tools.ts +364 -0
- package/mcp/tools/index.ts +4 -0
- package/package.json +52 -0
- package/rispecs/00-coaiajs-platform.spec.md +102 -0
- package/rispecs/01-core-config.spec.md +89 -0
- package/rispecs/02-redis-module.spec.md +62 -0
- package/rispecs/03-langfuse-module.spec.md +101 -0
- package/rispecs/04-narrative-engine.spec.md +185 -0
- package/rispecs/05-pde-engine.spec.md +112 -0
- package/rispecs/06-planning-engine.spec.md +128 -0
- package/rispecs/07-pipeline-templates.spec.md +97 -0
- package/rispecs/08-cli-interface.spec.md +134 -0
- package/rispecs/09-mcp-server.spec.md +140 -0
- package/rispecs/10-audio-module.spec.md +63 -0
- package/rispecs/KINSHIP.md +56 -0
- package/rispecs/README.md +100 -0
- package/src/audio.ts +76 -0
- package/src/cli-helpers.ts +86 -0
- package/src/cli.ts +1223 -0
- package/src/config.ts +172 -0
- package/src/environment.ts +171 -0
- package/src/github.ts +143 -0
- package/src/langfuse/client.ts +105 -0
- package/src/langfuse/comments.ts +52 -0
- package/src/langfuse/datasets.ts +178 -0
- package/src/langfuse/index.ts +33 -0
- package/src/langfuse/media.ts +193 -0
- package/src/langfuse/observations.ts +131 -0
- package/src/langfuse/prompts.ts +157 -0
- package/src/langfuse/scores.ts +456 -0
- package/src/langfuse/traces.ts +276 -0
- package/src/llm.ts +106 -0
- package/src/narrative/graph-manager.ts +1358 -0
- package/src/narrative/index.ts +32 -0
- package/src/narrative/markdown-export.ts +535 -0
- package/src/narrative/tool-definitions.ts +635 -0
- package/src/narrative/tool-handlers.ts +528 -0
- package/src/narrative/types.ts +9 -0
- package/src/narrative/validation.ts +179 -0
- package/src/pde/index.ts +8 -0
- package/src/pde/mcp-handlers.ts +359 -0
- package/src/pde/mcp-tools.ts +201 -0
- package/src/pde/session-manager.ts +248 -0
- package/src/pde/stc-mapper.ts +298 -0
- package/src/pipeline/index.ts +7 -0
- package/src/pipeline/template-engine.ts +398 -0
- package/src/planning/index.ts +13 -0
- package/src/planning/mcp-handlers.ts +369 -0
- package/src/planning/mcp-tools.ts +155 -0
- package/src/planning/plan-parser.ts +587 -0
- package/src/redis.ts +97 -0
- package/src/types.ts +280 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# @aws-sdk/client-polly: Technical Assessment for CoAiA.js
|
|
2
|
+
|
|
3
|
+
> Package selection brief — Text-to-speech synthesis replacing coaiapy's boto3-based syntation.py
|
|
4
|
+
|
|
5
|
+
## Summary & Recommendation
|
|
6
|
+
|
|
7
|
+
**Use `@aws-sdk/client-polly` v3.x** (currently 3.997.0). The AWS SDK v3 modular architecture means we import only the Polly client — not the entire AWS SDK. Streaming audio responses pipe directly to files or downstream consumers. This replaces coaiapy's `syntation.py` which uses boto3 with manually configured credentials.
|
|
8
|
+
|
|
9
|
+
**Pin:** `"@aws-sdk/client-polly": "^3.700.0"`
|
|
10
|
+
|
|
11
|
+
## What We're Replacing
|
|
12
|
+
|
|
13
|
+
Coaiapy's `syntation.py` uses boto3 for AWS Polly:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# coaiapy/syntation.py — boto3-based Polly
|
|
17
|
+
import boto3
|
|
18
|
+
from coaiamodule import read_config
|
|
19
|
+
|
|
20
|
+
def synthesize(text, voice_id, outfile, outformat="mp3"):
|
|
21
|
+
config = read_config()
|
|
22
|
+
key = config["pollyconf"]["key"]
|
|
23
|
+
secret = config["pollyconf"]["secret"]
|
|
24
|
+
region = config["pollyconf"]["region"]
|
|
25
|
+
|
|
26
|
+
polly = boto3.client('polly',
|
|
27
|
+
aws_access_key_id=key,
|
|
28
|
+
aws_secret_access_key=secret,
|
|
29
|
+
region_name=region)
|
|
30
|
+
response = polly.synthesize_speech(
|
|
31
|
+
Text=text,
|
|
32
|
+
OutputFormat=outformat,
|
|
33
|
+
VoiceId=voice_id
|
|
34
|
+
)
|
|
35
|
+
# Write audio stream to file
|
|
36
|
+
with open(outfile, 'wb') as f:
|
|
37
|
+
f.write(response['AudioStream'].read())
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Key issues: monolithic boto3 import (~100MB installed), manual credential management from config file, synchronous blocking I/O.
|
|
41
|
+
|
|
42
|
+
## Options Compared
|
|
43
|
+
|
|
44
|
+
| Feature | @aws-sdk/client-polly v3 | boto3 (Python) | AWS SDK v2 (deprecated) |
|
|
45
|
+
|---------|------------------------|----------------|----------------------|
|
|
46
|
+
| Install size | ~5MB (just Polly) | ~100MB (all of boto3) | ~40MB (entire aws-sdk) |
|
|
47
|
+
| Modular imports | ✅ Only Polly client | ❌ Entire SDK | ❌ Entire SDK |
|
|
48
|
+
| Streaming response | ✅ Node.js Readable stream | ⚠️ StreamingBody | ✅ |
|
|
49
|
+
| Credential chain | ✅ Automatic (env, profile, IMDS) | ✅ Automatic | ✅ Automatic |
|
|
50
|
+
| TypeScript | Full native types | N/A | @types/aws-sdk |
|
|
51
|
+
| Async/await | ✅ Native | ✅ | ⚠️ Callback + promise |
|
|
52
|
+
| End of support | Active | Active | September 2025 ❌ |
|
|
53
|
+
| SSML support | ✅ | ✅ | ✅ |
|
|
54
|
+
| Neural voices | ✅ | ✅ | ✅ |
|
|
55
|
+
|
|
56
|
+
## API Overview
|
|
57
|
+
|
|
58
|
+
### Basic Speech Synthesis
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { PollyClient, SynthesizeSpeechCommand } from '@aws-sdk/client-polly';
|
|
62
|
+
import { Readable } from 'stream';
|
|
63
|
+
import { createWriteStream } from 'fs';
|
|
64
|
+
import { pipeline } from 'stream/promises';
|
|
65
|
+
import { loadConfig } from '../config.js';
|
|
66
|
+
|
|
67
|
+
const config = loadConfig();
|
|
68
|
+
|
|
69
|
+
const polly = new PollyClient({
|
|
70
|
+
region: config.awsRegion ?? 'us-east-1',
|
|
71
|
+
// Credentials auto-resolve from env/profile/config
|
|
72
|
+
// Or explicit:
|
|
73
|
+
credentials: config.pollyKey ? {
|
|
74
|
+
accessKeyId: config.pollyKey,
|
|
75
|
+
secretAccessKey: config.pollySecret!,
|
|
76
|
+
} : undefined,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function synthesize(
|
|
80
|
+
text: string,
|
|
81
|
+
voiceId: string,
|
|
82
|
+
outfile: string,
|
|
83
|
+
format: 'mp3' | 'ogg_vorbis' | 'pcm' = 'mp3'
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const command = new SynthesizeSpeechCommand({
|
|
86
|
+
Text: text,
|
|
87
|
+
VoiceId: voiceId,
|
|
88
|
+
OutputFormat: format,
|
|
89
|
+
Engine: 'neural', // Use neural voices for quality
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const response = await polly.send(command);
|
|
93
|
+
|
|
94
|
+
if (response.AudioStream instanceof Readable) {
|
|
95
|
+
await pipeline(response.AudioStream, createWriteStream(outfile));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### SSML Support (Rich Speech Control)
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
async function synthesizeSSML(
|
|
104
|
+
ssml: string,
|
|
105
|
+
voiceId: string,
|
|
106
|
+
outfile: string
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const command = new SynthesizeSpeechCommand({
|
|
109
|
+
Text: ssml,
|
|
110
|
+
TextType: 'ssml',
|
|
111
|
+
VoiceId: voiceId,
|
|
112
|
+
OutputFormat: 'mp3',
|
|
113
|
+
Engine: 'neural',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const response = await polly.send(command);
|
|
117
|
+
if (response.AudioStream instanceof Readable) {
|
|
118
|
+
await pipeline(response.AudioStream, createWriteStream(outfile));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Usage
|
|
123
|
+
await synthesizeSSML(
|
|
124
|
+
`<speak>
|
|
125
|
+
<prosody rate="slow">Welcome to CoAiA.</prosody>
|
|
126
|
+
<break time="500ms"/>
|
|
127
|
+
Your structural tension chart has <emphasis>three</emphasis> pending actions.
|
|
128
|
+
</speak>`,
|
|
129
|
+
'Joanna',
|
|
130
|
+
'output.mp3'
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Long-Form Synthesis (Start Task)
|
|
135
|
+
|
|
136
|
+
For texts >3000 characters, use the async task API:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import {
|
|
140
|
+
PollyClient,
|
|
141
|
+
StartSpeechSynthesisTaskCommand,
|
|
142
|
+
GetSpeechSynthesisTaskCommand
|
|
143
|
+
} from '@aws-sdk/client-polly';
|
|
144
|
+
|
|
145
|
+
async function synthesizeLongForm(
|
|
146
|
+
text: string,
|
|
147
|
+
voiceId: string,
|
|
148
|
+
s3Bucket: string,
|
|
149
|
+
s3Key: string
|
|
150
|
+
): Promise<string> {
|
|
151
|
+
const command = new StartSpeechSynthesisTaskCommand({
|
|
152
|
+
Text: text,
|
|
153
|
+
VoiceId: voiceId,
|
|
154
|
+
OutputFormat: 'mp3',
|
|
155
|
+
OutputS3BucketName: s3Bucket,
|
|
156
|
+
OutputS3KeyPrefix: s3Key,
|
|
157
|
+
Engine: 'neural',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const response = await polly.send(command);
|
|
161
|
+
const taskId = response.SynthesisTask?.TaskId;
|
|
162
|
+
|
|
163
|
+
// Poll for completion
|
|
164
|
+
let status = 'inProgress';
|
|
165
|
+
while (status === 'inProgress' || status === 'scheduled') {
|
|
166
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
167
|
+
const task = await polly.send(
|
|
168
|
+
new GetSpeechSynthesisTaskCommand({ TaskId: taskId })
|
|
169
|
+
);
|
|
170
|
+
status = task.SynthesisTask?.TaskStatus ?? 'failed';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return response.SynthesisTask?.OutputUri ?? '';
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Voice Listing
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { DescribeVoicesCommand } from '@aws-sdk/client-polly';
|
|
181
|
+
|
|
182
|
+
async function listVoices(language?: string): Promise<{ id: string; name: string; engine: string }[]> {
|
|
183
|
+
const command = new DescribeVoicesCommand({
|
|
184
|
+
LanguageCode: language,
|
|
185
|
+
Engine: 'neural',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const response = await polly.send(command);
|
|
189
|
+
return (response.Voices ?? []).map(v => ({
|
|
190
|
+
id: v.Id!,
|
|
191
|
+
name: v.Name!,
|
|
192
|
+
engine: v.SupportedEngines?.join(', ') ?? 'standard',
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Stream to Buffer (for API responses)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
async function synthesizeToBuffer(text: string, voiceId: string): Promise<Buffer> {
|
|
201
|
+
const command = new SynthesizeSpeechCommand({
|
|
202
|
+
Text: text,
|
|
203
|
+
VoiceId: voiceId,
|
|
204
|
+
OutputFormat: 'mp3',
|
|
205
|
+
Engine: 'neural',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const response = await polly.send(command);
|
|
209
|
+
|
|
210
|
+
if (response.AudioStream instanceof Readable) {
|
|
211
|
+
const chunks: Buffer[] = [];
|
|
212
|
+
for await (const chunk of response.AudioStream) {
|
|
213
|
+
chunks.push(Buffer.from(chunk));
|
|
214
|
+
}
|
|
215
|
+
return Buffer.concat(chunks);
|
|
216
|
+
}
|
|
217
|
+
throw new Error('No audio stream in response');
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Integration Plan
|
|
222
|
+
|
|
223
|
+
1. **Core module:** `src/polly/client.ts` — PollyClient singleton from config
|
|
224
|
+
2. **Synthesis:** `src/polly/synthesize.ts` — text/SSML → audio file/buffer
|
|
225
|
+
3. **Voice management:** `src/polly/voices.ts` — list/select voices
|
|
226
|
+
4. **CLI command:** `src/commands/speak.ts` or integrate into pipeline steps
|
|
227
|
+
5. **MCP tool:** `mcp/tools/speech.ts` — speech synthesis as an agent tool
|
|
228
|
+
6. **Credential pattern:** AWS credential chain (env → profile → config file)
|
|
229
|
+
|
|
230
|
+
### Credential Migration
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// BEFORE (coaiapy — manual from config)
|
|
234
|
+
key = config["pollyconf"]["key"]
|
|
235
|
+
secret = config["pollyconf"]["secret"]
|
|
236
|
+
|
|
237
|
+
// AFTER (coaiajs — AWS credential chain)
|
|
238
|
+
// Option 1: Environment variables (recommended)
|
|
239
|
+
// AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
|
|
240
|
+
|
|
241
|
+
// Option 2: Explicit from coaia config (backward compat)
|
|
242
|
+
const polly = new PollyClient({
|
|
243
|
+
region: config.awsRegion,
|
|
244
|
+
credentials: config.pollyKey ? {
|
|
245
|
+
accessKeyId: config.pollyKey,
|
|
246
|
+
secretAccessKey: config.pollySecret!,
|
|
247
|
+
} : undefined, // falls back to default credential chain
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Version & Ecosystem
|
|
252
|
+
|
|
253
|
+
| Metric | Value |
|
|
254
|
+
|--------|-------|
|
|
255
|
+
| Current version | 3.997.0 (Feb 2026) |
|
|
256
|
+
| Install size | ~5MB (Polly client only) |
|
|
257
|
+
| TypeScript | Full native types |
|
|
258
|
+
| Node.js compat | ≥16 (we target ≥20) |
|
|
259
|
+
| Streaming | Native Node.js Readable |
|
|
260
|
+
| Credential chain | env → profile → IMDS → config |
|
|
261
|
+
| Neural voices | 60+ voices, 30+ languages |
|
|
262
|
+
| SSML | Full support |
|
|
263
|
+
| License | Apache-2.0 |
|
|
264
|
+
|
|
265
|
+
## References
|
|
266
|
+
|
|
267
|
+
- npm: https://www.npmjs.com/package/@aws-sdk/client-polly
|
|
268
|
+
- AWS Polly docs: https://docs.aws.amazon.com/polly/
|
|
269
|
+
- SDK v3 migration: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/
|
|
270
|
+
- v2 EOL notice: https://www.npmjs.com/package/aws-sdk
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Commander.js CLI Framework: Technical Assessment for CoAiA.js
|
|
2
|
+
|
|
3
|
+
> Package selection brief — CLI framework for coaiajs's multi-subcommand interface (tash, fetch, fuse, pipeline, env, gh, narrative, pde, planning)
|
|
4
|
+
|
|
5
|
+
## Summary & Recommendation
|
|
6
|
+
|
|
7
|
+
**Use `commander` v12.x** for the coaiajs CLI. Commander's Git-style subcommand model maps directly to our 9+ subcommand structure. It's the most widely adopted Node.js CLI framework (weekly downloads dwarf alternatives), has zero dependencies, generates help automatically, and every team member already knows it. Clipanion's class-based approach is elegant but niche; yargs is powerful but over-engineered for our tree.
|
|
8
|
+
|
|
9
|
+
**Pin:** `"commander": "^12.1.0"`
|
|
10
|
+
|
|
11
|
+
## What We're Replacing
|
|
12
|
+
|
|
13
|
+
**Coaiapy** uses Python's `argparse` with subparsers:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# coaiapy/coaiacli.py — argparse subcommand pattern
|
|
17
|
+
import argparse
|
|
18
|
+
parser = argparse.ArgumentParser(description='CoAiA CLI')
|
|
19
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
20
|
+
parser_fuse = subparsers.add_parser('fuse', help='Manage Langfuse integrations.')
|
|
21
|
+
parser_gh = subparsers.add_parser('gh', help='GitHub operations.')
|
|
22
|
+
# ... 9+ subcommands
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**coaia-narrative** uses `minimist` — bare-bones argv parsing with no help generation:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// coaia-narrative/src/cli.ts — minimist pattern
|
|
29
|
+
import minimist from 'minimist';
|
|
30
|
+
const args = minimist(process.argv.slice(2));
|
|
31
|
+
// Manual flag handling: --memory-path, -M, --json, --no-color, --interactive
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**coaia-planning** also uses `minimist` for `--plans-dir` and `--output-dir`.
|
|
35
|
+
|
|
36
|
+
## Options Compared
|
|
37
|
+
|
|
38
|
+
| Feature | Commander v12 | Yargs v17 | Clipanion v4 | minimist v1.2 |
|
|
39
|
+
|---------|--------------|-----------|-------------|--------------|
|
|
40
|
+
| Weekly downloads | ~125M | ~80M | ~4M | ~50M |
|
|
41
|
+
| Dependencies | 0 | 5+ | 0 | 0 |
|
|
42
|
+
| Subcommand model | Git-style `.command()` | Builder callbacks | Class-based `paths` | None (manual) |
|
|
43
|
+
| Auto-help | ✅ Excellent | ✅ Excellent | ✅ Good | ❌ None |
|
|
44
|
+
| TypeScript | Good (DefinitelyTyped) | Good | Excellent (first-class) | Minimal |
|
|
45
|
+
| Learning curve | Low | Medium | Medium-high | Very low |
|
|
46
|
+
| Argument parsing | Options, args, variadic | All types + coercion | All types + validation | Raw key-value |
|
|
47
|
+
| Completion (bash/zsh) | Plugin available | Built-in | Not built-in | ❌ |
|
|
48
|
+
| Version handling | Built-in `.version()` | Built-in | Manual | ❌ |
|
|
49
|
+
| Nested subcommands | `.command()` nesting | Recursive builders | Arbitrary `paths` depth | ❌ |
|
|
50
|
+
| Exit override | `.exitOverride()` for testing | `fail()` handler | Exception-based | N/A |
|
|
51
|
+
|
|
52
|
+
## API Overview
|
|
53
|
+
|
|
54
|
+
### Main CLI Entry Point
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// src/cli.ts
|
|
58
|
+
import { Command } from 'commander';
|
|
59
|
+
import { version } from '../package.json';
|
|
60
|
+
|
|
61
|
+
const program = new Command()
|
|
62
|
+
.name('coaia')
|
|
63
|
+
.version(version)
|
|
64
|
+
.description('CoAiA.js — Structural Tension Agent Framework');
|
|
65
|
+
|
|
66
|
+
// Register subcommands
|
|
67
|
+
program.addCommand(createTashCommand());
|
|
68
|
+
program.addCommand(createFetchCommand());
|
|
69
|
+
program.addCommand(createFuseCommand());
|
|
70
|
+
program.addCommand(createPipelineCommand());
|
|
71
|
+
program.addCommand(createEnvCommand());
|
|
72
|
+
program.addCommand(createGhCommand());
|
|
73
|
+
program.addCommand(createNarrativeCommand());
|
|
74
|
+
program.addCommand(createPdeCommand());
|
|
75
|
+
program.addCommand(createPlanningCommand());
|
|
76
|
+
|
|
77
|
+
program.parse();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Subcommand Definition Pattern
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// src/commands/fuse.ts
|
|
84
|
+
import { Command } from 'commander';
|
|
85
|
+
|
|
86
|
+
export function createFuseCommand(): Command {
|
|
87
|
+
const fuse = new Command('fuse')
|
|
88
|
+
.description('Manage Langfuse integrations');
|
|
89
|
+
|
|
90
|
+
fuse
|
|
91
|
+
.command('trace')
|
|
92
|
+
.description('Create or list traces')
|
|
93
|
+
.option('-s, --session <id>', 'Session ID')
|
|
94
|
+
.option('--list', 'List recent traces')
|
|
95
|
+
.option('--json', 'Output as JSON')
|
|
96
|
+
.action(async (opts) => {
|
|
97
|
+
if (opts.list) {
|
|
98
|
+
const traces = await listTraces(opts);
|
|
99
|
+
console.log(opts.json ? JSON.stringify(traces) : formatTable(traces));
|
|
100
|
+
} else {
|
|
101
|
+
await createTrace(opts);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
fuse
|
|
106
|
+
.command('score')
|
|
107
|
+
.description('Score a trace')
|
|
108
|
+
.requiredOption('-t, --trace-id <id>', 'Trace ID to score')
|
|
109
|
+
.requiredOption('-n, --name <name>', 'Score name')
|
|
110
|
+
.requiredOption('-v, --value <number>', 'Score value', parseFloat)
|
|
111
|
+
.action(async (opts) => {
|
|
112
|
+
await scoreTrace(opts.traceId, opts.name, opts.value);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return fuse;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### GitHub Subcommand (replacing cogh.py)
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// src/commands/gh.ts
|
|
123
|
+
import { Command } from 'commander';
|
|
124
|
+
|
|
125
|
+
export function createGhCommand(): Command {
|
|
126
|
+
const gh = new Command('gh')
|
|
127
|
+
.description('GitHub operations');
|
|
128
|
+
|
|
129
|
+
gh.command('issues')
|
|
130
|
+
.description('List repository issues')
|
|
131
|
+
.argument('<owner>', 'Repository owner')
|
|
132
|
+
.argument('<repo>', 'Repository name')
|
|
133
|
+
.option('--state <state>', 'Filter by state', 'open')
|
|
134
|
+
.option('--labels <labels...>', 'Filter by labels')
|
|
135
|
+
.option('--json', 'JSON output')
|
|
136
|
+
.action(async (owner, repo, opts) => {
|
|
137
|
+
const issues = await listIssues(owner, repo, opts);
|
|
138
|
+
console.log(opts.json ? JSON.stringify(issues) : formatIssuesTable(issues));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
gh.command('issue <owner> <repo> <number>')
|
|
142
|
+
.description('Get a specific issue')
|
|
143
|
+
.action(async (owner, repo, number) => {
|
|
144
|
+
const issue = await getIssue(owner, repo, parseInt(number));
|
|
145
|
+
console.log(JSON.stringify(issue, null, 2));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return gh;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Pipeline Subcommand (with YAML template loading)
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// src/commands/pipeline.ts
|
|
156
|
+
import { Command } from 'commander';
|
|
157
|
+
|
|
158
|
+
export function createPipelineCommand(): Command {
|
|
159
|
+
const pipeline = new Command('pipeline')
|
|
160
|
+
.description('Execute pipeline templates');
|
|
161
|
+
|
|
162
|
+
pipeline
|
|
163
|
+
.command('run <template>')
|
|
164
|
+
.description('Run a pipeline template')
|
|
165
|
+
.option('-v, --var <key=value...>', 'Template variables')
|
|
166
|
+
.option('--dry-run', 'Show execution plan without running')
|
|
167
|
+
.option('--template-dir <dir>', 'Template search directory')
|
|
168
|
+
.action(async (template, opts) => {
|
|
169
|
+
const vars = parseVars(opts.var ?? []);
|
|
170
|
+
await runPipeline(template, vars, opts);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
pipeline
|
|
174
|
+
.command('list')
|
|
175
|
+
.description('List available templates')
|
|
176
|
+
.action(async () => {
|
|
177
|
+
const templates = await listTemplates();
|
|
178
|
+
templates.forEach(t => console.log(` ${t.name} (v${t.version})`));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return pipeline;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Global Options Pattern
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// Shared options across all subcommands
|
|
189
|
+
program
|
|
190
|
+
.option('--json', 'Output as JSON')
|
|
191
|
+
.option('--no-color', 'Disable colored output')
|
|
192
|
+
.option('--verbose', 'Verbose logging')
|
|
193
|
+
.option('-M, --memory-path <path>', 'Memory file path')
|
|
194
|
+
.option('--env <file>', 'Custom env file');
|
|
195
|
+
|
|
196
|
+
// Access in any subcommand action
|
|
197
|
+
function getGlobalOpts(cmd: Command) {
|
|
198
|
+
const root = cmd.parent ?? cmd;
|
|
199
|
+
return root.opts() as { json?: boolean; color?: boolean; verbose?: boolean; memoryPath?: string };
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Integration Plan
|
|
204
|
+
|
|
205
|
+
1. **Entry point:** `src/cli.ts` — Commander program with `.parse()`
|
|
206
|
+
2. **Command directory:** `src/commands/` — one file per subcommand
|
|
207
|
+
- `tash.ts` — Task management (structural tension)
|
|
208
|
+
- `fetch.ts` — Data fetching operations
|
|
209
|
+
- `fuse.ts` — Langfuse integration
|
|
210
|
+
- `pipeline.ts` — Pipeline template execution
|
|
211
|
+
- `env.ts` — Environment management
|
|
212
|
+
- `gh.ts` — GitHub operations
|
|
213
|
+
- `narrative.ts` — Narrative/chart operations
|
|
214
|
+
- `pde.ts` — Prompt decomposition
|
|
215
|
+
- `planning.ts` — Plan-to-STC operations
|
|
216
|
+
3. **Bin entry:** `package.json` → `"bin": { "coaia": "./dist/src/cli.js" }`
|
|
217
|
+
4. **Testing:** Commander's `.exitOverride()` + `.configureOutput()` for unit tests
|
|
218
|
+
|
|
219
|
+
### Migration Path from minimist
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// BEFORE (coaia-narrative minimist pattern)
|
|
223
|
+
const args = minimist(process.argv.slice(2));
|
|
224
|
+
const memoryPath = args['memory-path'] || args.M || process.env.COAIA_MEMORY_PATH;
|
|
225
|
+
|
|
226
|
+
// AFTER (Commander)
|
|
227
|
+
program.option('-M, --memory-path <path>', 'Memory file path', process.env.COAIA_MEMORY_PATH);
|
|
228
|
+
// memoryPath is now typed and auto-documented in --help
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Version & Ecosystem
|
|
232
|
+
|
|
233
|
+
| Metric | Value |
|
|
234
|
+
|--------|-------|
|
|
235
|
+
| Current version | 12.1.0 |
|
|
236
|
+
| Weekly downloads | ~125M |
|
|
237
|
+
| Dependencies | 0 |
|
|
238
|
+
| TypeScript | `@types/commander` (DefinitelyTyped) |
|
|
239
|
+
| GitHub stars | ~27,000 |
|
|
240
|
+
| Used by | npm CLI, Vue CLI, create-react-app |
|
|
241
|
+
| Node.js compat | ≥16 (we target ≥20) |
|
|
242
|
+
| License | MIT |
|
|
243
|
+
|
|
244
|
+
## Why Not Yargs?
|
|
245
|
+
|
|
246
|
+
Yargs is powerful but overkill. Our subcommand tree is wide (9 commands) but not deeply nested. Commander's chainable API makes each command file self-contained and readable. Yargs' middleware system and callback-based builders add complexity we don't need.
|
|
247
|
+
|
|
248
|
+
## Why Not Clipanion?
|
|
249
|
+
|
|
250
|
+
Clipanion's TypeScript-first class-based approach is architecturally beautiful, but:
|
|
251
|
+
1. ~4M weekly downloads vs Commander's ~125M = smaller help ecosystem
|
|
252
|
+
2. Class-based commands are less familiar to contributors
|
|
253
|
+
3. Yarn is its primary consumer; documentation coverage is thin for edge cases
|
|
254
|
+
4. Commander is already in our `package.json`
|
|
255
|
+
|
|
256
|
+
## References
|
|
257
|
+
|
|
258
|
+
- npm: https://www.npmjs.com/package/commander
|
|
259
|
+
- GitHub: https://github.com/tj/commander.js
|
|
260
|
+
- Guide: https://betterstack.com/community/guides/scaling-nodejs/commander-explained/
|
|
261
|
+
- Comparison: https://npm-compare.com/commander,yargs
|
|
262
|
+
- Clipanion: https://github.com/arcanis/clipanion
|