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.
Files changed (162) hide show
  1. package/CLAUDE.md +56 -0
  2. package/KINSHIP.md +63 -0
  3. package/README.md +148 -0
  4. package/articles/academic/creative-orientation-vs-problem-solving.md +177 -0
  5. package/articles/academic/jsonl-knowledge-graphs-agent-memory.md +142 -0
  6. package/articles/academic/langfuse-observability-llm-pipelines.md +144 -0
  7. package/articles/academic/medicine-wheel-software-architecture.md +163 -0
  8. package/articles/academic/mmot-autonomous-agents.md +156 -0
  9. package/articles/academic/model-context-protocol-interagent.md +161 -0
  10. package/articles/academic/pde-prompt-decomposition.md +186 -0
  11. package/articles/academic/structural-tension-in-ai-agents.md +134 -0
  12. package/articles/reviews/mcp-protocol-design-review.md +170 -0
  13. package/articles/reviews/observability-ai-systems-review.md +176 -0
  14. package/articles/reviews/prompt-engineering-decomposition-review.md +184 -0
  15. package/articles/surveys/agent-orchestration-survey.md +186 -0
  16. package/articles/surveys/knowledge-graph-storage-survey.md +204 -0
  17. package/articles/surveys/structural-tension-methodology-survey.md +154 -0
  18. package/articles/technical/aws-sdk-v3-polly.md +270 -0
  19. package/articles/technical/commander-cli-framework.md +262 -0
  20. package/articles/technical/dotenv-config-patterns.md +360 -0
  21. package/articles/technical/ioredis-vs-redis.md +142 -0
  22. package/articles/technical/langfuse-js-sdk-vs-rest.md +191 -0
  23. package/articles/technical/mcp-sdk-typescript.md +291 -0
  24. package/articles/technical/octokit-github-api.md +293 -0
  25. package/articles/technical/openai-sdk-modern.md +231 -0
  26. package/articles/technical/yaml-parsing-node.md +266 -0
  27. package/articles/technical/zod-runtime-validation.md +212 -0
  28. package/dist/mcp/config.d.ts +14 -0
  29. package/dist/mcp/config.js +185 -0
  30. package/dist/mcp/server.d.ts +3 -0
  31. package/dist/mcp/server.js +324 -0
  32. package/dist/mcp/tools/coaiapy-tools.d.ts +8 -0
  33. package/dist/mcp/tools/coaiapy-tools.js +326 -0
  34. package/dist/mcp/tools/index.d.ts +3 -0
  35. package/dist/mcp/tools/index.js +3 -0
  36. package/dist/src/audio.d.ts +12 -0
  37. package/dist/src/audio.js +57 -0
  38. package/dist/src/cli-helpers.d.ts +9 -0
  39. package/dist/src/cli-helpers.js +78 -0
  40. package/dist/src/cli.d.ts +3 -0
  41. package/dist/src/cli.js +1086 -0
  42. package/dist/src/config.d.ts +15 -0
  43. package/dist/src/config.js +145 -0
  44. package/dist/src/environment.d.ts +44 -0
  45. package/dist/src/environment.js +146 -0
  46. package/dist/src/github.d.ts +47 -0
  47. package/dist/src/github.js +79 -0
  48. package/dist/src/langfuse/client.d.ts +30 -0
  49. package/dist/src/langfuse/client.js +75 -0
  50. package/dist/src/langfuse/comments.d.ts +16 -0
  51. package/dist/src/langfuse/comments.js +36 -0
  52. package/dist/src/langfuse/datasets.d.ts +19 -0
  53. package/dist/src/langfuse/datasets.js +140 -0
  54. package/dist/src/langfuse/index.d.ts +13 -0
  55. package/dist/src/langfuse/index.js +10 -0
  56. package/dist/src/langfuse/media.d.ts +11 -0
  57. package/dist/src/langfuse/media.js +167 -0
  58. package/dist/src/langfuse/observations.d.ts +18 -0
  59. package/dist/src/langfuse/observations.js +113 -0
  60. package/dist/src/langfuse/prompts.d.ts +14 -0
  61. package/dist/src/langfuse/prompts.js +127 -0
  62. package/dist/src/langfuse/scores.d.ts +54 -0
  63. package/dist/src/langfuse/scores.js +366 -0
  64. package/dist/src/langfuse/traces.d.ts +29 -0
  65. package/dist/src/langfuse/traces.js +225 -0
  66. package/dist/src/llm.d.ts +23 -0
  67. package/dist/src/llm.js +76 -0
  68. package/dist/src/narrative/graph-manager.d.ts +106 -0
  69. package/dist/src/narrative/graph-manager.js +915 -0
  70. package/dist/src/narrative/index.d.ts +9 -0
  71. package/dist/src/narrative/index.js +7 -0
  72. package/dist/src/narrative/markdown-export.d.ts +21 -0
  73. package/dist/src/narrative/markdown-export.js +383 -0
  74. package/dist/src/narrative/tool-definitions.d.ts +21 -0
  75. package/dist/src/narrative/tool-definitions.js +588 -0
  76. package/dist/src/narrative/tool-handlers.d.ts +11 -0
  77. package/dist/src/narrative/tool-handlers.js +462 -0
  78. package/dist/src/narrative/types.d.ts +2 -0
  79. package/dist/src/narrative/types.js +2 -0
  80. package/dist/src/narrative/validation.d.ts +29 -0
  81. package/dist/src/narrative/validation.js +144 -0
  82. package/dist/src/pde/index.d.ts +8 -0
  83. package/dist/src/pde/index.js +8 -0
  84. package/dist/src/pde/mcp-handlers.d.ts +7 -0
  85. package/dist/src/pde/mcp-handlers.js +300 -0
  86. package/dist/src/pde/mcp-tools.d.ts +15 -0
  87. package/dist/src/pde/mcp-tools.js +191 -0
  88. package/dist/src/pde/session-manager.d.ts +32 -0
  89. package/dist/src/pde/session-manager.js +205 -0
  90. package/dist/src/pde/stc-mapper.d.ts +26 -0
  91. package/dist/src/pde/stc-mapper.js +234 -0
  92. package/dist/src/pipeline/index.d.ts +2 -0
  93. package/dist/src/pipeline/index.js +3 -0
  94. package/dist/src/pipeline/template-engine.d.ts +20 -0
  95. package/dist/src/pipeline/template-engine.js +361 -0
  96. package/dist/src/planning/index.d.ts +7 -0
  97. package/dist/src/planning/index.js +7 -0
  98. package/dist/src/planning/mcp-handlers.d.ts +7 -0
  99. package/dist/src/planning/mcp-handlers.js +310 -0
  100. package/dist/src/planning/mcp-tools.d.ts +15 -0
  101. package/dist/src/planning/mcp-tools.js +145 -0
  102. package/dist/src/planning/plan-parser.d.ts +35 -0
  103. package/dist/src/planning/plan-parser.js +506 -0
  104. package/dist/src/redis.d.ts +21 -0
  105. package/dist/src/redis.js +78 -0
  106. package/dist/src/types.d.ts +244 -0
  107. package/dist/src/types.js +8 -0
  108. package/mcp/config.ts +196 -0
  109. package/mcp/server.ts +402 -0
  110. package/mcp/tools/coaiapy-tools.ts +364 -0
  111. package/mcp/tools/index.ts +4 -0
  112. package/package.json +52 -0
  113. package/rispecs/00-coaiajs-platform.spec.md +102 -0
  114. package/rispecs/01-core-config.spec.md +89 -0
  115. package/rispecs/02-redis-module.spec.md +62 -0
  116. package/rispecs/03-langfuse-module.spec.md +101 -0
  117. package/rispecs/04-narrative-engine.spec.md +185 -0
  118. package/rispecs/05-pde-engine.spec.md +112 -0
  119. package/rispecs/06-planning-engine.spec.md +128 -0
  120. package/rispecs/07-pipeline-templates.spec.md +97 -0
  121. package/rispecs/08-cli-interface.spec.md +134 -0
  122. package/rispecs/09-mcp-server.spec.md +140 -0
  123. package/rispecs/10-audio-module.spec.md +63 -0
  124. package/rispecs/KINSHIP.md +56 -0
  125. package/rispecs/README.md +100 -0
  126. package/src/audio.ts +76 -0
  127. package/src/cli-helpers.ts +86 -0
  128. package/src/cli.ts +1223 -0
  129. package/src/config.ts +172 -0
  130. package/src/environment.ts +171 -0
  131. package/src/github.ts +143 -0
  132. package/src/langfuse/client.ts +105 -0
  133. package/src/langfuse/comments.ts +52 -0
  134. package/src/langfuse/datasets.ts +178 -0
  135. package/src/langfuse/index.ts +33 -0
  136. package/src/langfuse/media.ts +193 -0
  137. package/src/langfuse/observations.ts +131 -0
  138. package/src/langfuse/prompts.ts +157 -0
  139. package/src/langfuse/scores.ts +456 -0
  140. package/src/langfuse/traces.ts +276 -0
  141. package/src/llm.ts +106 -0
  142. package/src/narrative/graph-manager.ts +1358 -0
  143. package/src/narrative/index.ts +32 -0
  144. package/src/narrative/markdown-export.ts +535 -0
  145. package/src/narrative/tool-definitions.ts +635 -0
  146. package/src/narrative/tool-handlers.ts +528 -0
  147. package/src/narrative/types.ts +9 -0
  148. package/src/narrative/validation.ts +179 -0
  149. package/src/pde/index.ts +8 -0
  150. package/src/pde/mcp-handlers.ts +359 -0
  151. package/src/pde/mcp-tools.ts +201 -0
  152. package/src/pde/session-manager.ts +248 -0
  153. package/src/pde/stc-mapper.ts +298 -0
  154. package/src/pipeline/index.ts +7 -0
  155. package/src/pipeline/template-engine.ts +398 -0
  156. package/src/planning/index.ts +13 -0
  157. package/src/planning/mcp-handlers.ts +369 -0
  158. package/src/planning/mcp-tools.ts +155 -0
  159. package/src/planning/plan-parser.ts +587 -0
  160. package/src/redis.ts +97 -0
  161. package/src/types.ts +280 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * STC Mapper — Transforms mcp-pde DecompositionResult into Entity[]/Relation[]
3
+ * Ported from coaia-pde/src/stc-mapper.ts
4
+ */
5
+
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import type {
8
+ DecompositionResult,
9
+ Entity,
10
+ Relation,
11
+ EntityMetadata,
12
+ } from '../types.js';
13
+ import { URGENCY_DAYS } from '../types.js';
14
+
15
+ export class StcMapper {
16
+
17
+ /**
18
+ * Core method: Transform a DecompositionResult into Entity[]/Relation[].
19
+ */
20
+ mapDecompositionToChart(
21
+ result: DecompositionResult,
22
+ originalPrompt: string,
23
+ options?: { pdeId?: string; dueDate?: string }
24
+ ): { entities: Entity[]; relations: Relation[]; chartId: string } {
25
+ const chartId = `chart_${uuidv4().substring(0, 8)}`;
26
+ const now = new Date().toISOString();
27
+ const dueDate = options?.dueDate || this.calculateDueDate(result.primary.urgency);
28
+ const fourDirections = this.mapDirectionsToFourDirections(result.directions);
29
+
30
+ const entities: Entity[] = [];
31
+ const relations: Relation[] = [];
32
+
33
+ // 1. Master chart entity
34
+ entities.push({
35
+ name: `${chartId}_chart`,
36
+ entityType: 'structural_tension_chart',
37
+ observations: [`Master chart for: ${result.primary.action} ${result.primary.target}`],
38
+ metadata: {
39
+ chartId,
40
+ dueDate,
41
+ level: 0,
42
+ phase: 'germination',
43
+ createdAt: now,
44
+ updatedAt: now,
45
+ pdeId: options?.pdeId,
46
+ fourDirections,
47
+ }
48
+ });
49
+
50
+ // 2. Desired outcome entity
51
+ const desiredOutcomeObs = [this.formatDesiredOutcome(result.primary)];
52
+ for (const a of result.outputs.artifacts) desiredOutcomeObs.push(`Artifact: ${a}`);
53
+ for (const u of result.outputs.updates) desiredOutcomeObs.push(`Update: ${u}`);
54
+ for (const c of result.outputs.communications) desiredOutcomeObs.push(`Communication: ${c}`);
55
+
56
+ entities.push({
57
+ name: `${chartId}_desired_outcome`,
58
+ entityType: 'desired_outcome',
59
+ observations: desiredOutcomeObs,
60
+ metadata: {
61
+ chartId,
62
+ confidence: result.primary.confidence,
63
+ createdAt: now,
64
+ updatedAt: now,
65
+ }
66
+ });
67
+
68
+ // 3. Current reality entity
69
+ entities.push({
70
+ name: `${chartId}_current_reality`,
71
+ entityType: 'current_reality',
72
+ observations: this.formatCurrentReality(result.context, result.ambiguities),
73
+ metadata: {
74
+ chartId,
75
+ createdAt: now,
76
+ updatedAt: now,
77
+ }
78
+ });
79
+
80
+ // 4. Action step entities from secondary[] + actionStack[]
81
+ const totalActionCount = result.secondary.length + result.actionStack.length;
82
+ const actionDueDates = this.distributeActionDates(new Date(), new Date(dueDate), totalActionCount);
83
+ let actionIndex = 0;
84
+
85
+ for (const sec of result.secondary) {
86
+ const actionName = `${chartId}_action_${++actionIndex}`;
87
+
88
+ entities.push({
89
+ name: actionName,
90
+ entityType: 'action_step',
91
+ observations: [`${sec.action} ${sec.target}`],
92
+ metadata: {
93
+ chartId,
94
+ dueDate: actionDueDates[actionIndex - 1]?.toISOString(),
95
+ completionStatus: false,
96
+ level: 1,
97
+ parentChart: chartId,
98
+ implicit: sec.implicit,
99
+ confidence: sec.confidence,
100
+ createdAt: now,
101
+ updatedAt: now,
102
+ }
103
+ });
104
+
105
+ relations.push({
106
+ from: `${chartId}_chart`,
107
+ to: actionName,
108
+ relationType: 'has_action_step',
109
+ metadata: { createdAt: now }
110
+ });
111
+
112
+ relations.push({
113
+ from: actionName,
114
+ to: `${chartId}_desired_outcome`,
115
+ relationType: 'advances_toward',
116
+ metadata: { createdAt: now }
117
+ });
118
+
119
+ if (sec.dependency) {
120
+ this.addDependencyRelation(entities, relations, actionName, sec.dependency, now);
121
+ }
122
+ }
123
+
124
+ for (const item of result.actionStack) {
125
+ const actionName = `${chartId}_action_${++actionIndex}`;
126
+
127
+ entities.push({
128
+ name: actionName,
129
+ entityType: 'action_step',
130
+ observations: [item.text],
131
+ metadata: {
132
+ chartId,
133
+ dueDate: actionDueDates[actionIndex - 1]?.toISOString(),
134
+ completionStatus: item.completed || false,
135
+ level: 1,
136
+ parentChart: chartId,
137
+ direction: item.direction,
138
+ createdAt: now,
139
+ updatedAt: now,
140
+ }
141
+ });
142
+
143
+ relations.push({
144
+ from: `${chartId}_chart`,
145
+ to: actionName,
146
+ relationType: 'has_action_step',
147
+ metadata: { createdAt: now }
148
+ });
149
+
150
+ relations.push({
151
+ from: actionName,
152
+ to: `${chartId}_desired_outcome`,
153
+ relationType: 'advances_toward',
154
+ metadata: { createdAt: now }
155
+ });
156
+
157
+ if (item.dependency) {
158
+ this.addDependencyRelation(entities, relations, actionName, item.dependency, now);
159
+ }
160
+ }
161
+
162
+ // 5. Structural relations
163
+ relations.push(
164
+ {
165
+ from: `${chartId}_chart`,
166
+ to: `${chartId}_desired_outcome`,
167
+ relationType: 'has_desired_outcome',
168
+ metadata: { createdAt: now }
169
+ },
170
+ {
171
+ from: `${chartId}_chart`,
172
+ to: `${chartId}_current_reality`,
173
+ relationType: 'has_current_reality',
174
+ metadata: { createdAt: now }
175
+ },
176
+ {
177
+ from: `${chartId}_current_reality`,
178
+ to: `${chartId}_desired_outcome`,
179
+ relationType: 'creates_tension_with',
180
+ metadata: { createdAt: now }
181
+ }
182
+ );
183
+
184
+ return { entities, relations, chartId };
185
+ }
186
+
187
+ private addDependencyRelation(
188
+ entities: Entity[],
189
+ relations: Relation[],
190
+ fromName: string,
191
+ dependency: string,
192
+ now: string
193
+ ): void {
194
+ const depEntity = entities.find(
195
+ e => e.entityType === 'action_step' && e.observations[0]?.includes(dependency)
196
+ );
197
+ if (depEntity) {
198
+ relations.push({
199
+ from: fromName,
200
+ to: depEntity.name,
201
+ relationType: 'depends_on',
202
+ metadata: { createdAt: now, context: dependency }
203
+ });
204
+ }
205
+ }
206
+
207
+ private formatDesiredOutcome(primary: { action: string; target: string }): string {
208
+ const { action, target } = primary;
209
+
210
+ const resultPhrases: Record<string, string> = {
211
+ create: `Completed ${target}`,
212
+ build: `Functional ${target}`,
213
+ implement: `Working ${target}`,
214
+ write: `Finished ${target}`,
215
+ design: `Complete design for ${target}`,
216
+ develop: `Deployed ${target}`,
217
+ analyze: `Clear understanding of ${target}`,
218
+ integrate: `Fully integrated ${target}`,
219
+ test: `Validated ${target}`,
220
+ fix: `Resolved ${target}`,
221
+ refactor: `Refactored ${target}`,
222
+ };
223
+
224
+ for (const [verb, phrase] of Object.entries(resultPhrases)) {
225
+ if (action.startsWith(verb)) {
226
+ return phrase;
227
+ }
228
+ }
229
+
230
+ return `Completed: ${action} ${target}`;
231
+ }
232
+
233
+ private formatCurrentReality(
234
+ context: DecompositionResult['context'],
235
+ ambiguities: DecompositionResult['ambiguities']
236
+ ): string[] {
237
+ const observations: string[] = [];
238
+
239
+ if (context.files_needed.length > 0) {
240
+ observations.push(`Files to reference: ${context.files_needed.join(', ')}`);
241
+ }
242
+
243
+ if (context.tools_required.length > 0) {
244
+ observations.push(`Tools available: ${context.tools_required.join(', ')}`);
245
+ }
246
+
247
+ for (const assumption of context.assumptions) {
248
+ observations.push(`Assumption: ${assumption}`);
249
+ }
250
+
251
+ for (const ambiguity of ambiguities) {
252
+ observations.push(`⚠️ Ambiguity: ${ambiguity.text} — Suggestion: ${ambiguity.suggestion}`);
253
+ }
254
+
255
+ if (observations.length === 0) {
256
+ observations.push('Starting point: No prior work on this goal');
257
+ }
258
+
259
+ return observations;
260
+ }
261
+
262
+ private mapDirectionsToFourDirections(
263
+ directions: DecompositionResult['directions']
264
+ ): EntityMetadata['fourDirections'] {
265
+ const join = (items: Array<{ text: string }>) =>
266
+ items.length > 0 ? items.map(i => i.text).join('; ') : null;
267
+
268
+ return {
269
+ north_vision: join(directions.north || []),
270
+ east_intention: join(directions.east || []),
271
+ south_emotion: join(directions.south || []),
272
+ west_introspection: join(directions.west || []),
273
+ };
274
+ }
275
+
276
+ private calculateDueDate(urgency: string): string {
277
+ const days = URGENCY_DAYS[urgency] || URGENCY_DAYS['session'];
278
+ const date = new Date();
279
+ date.setDate(date.getDate() + days);
280
+ return date.toISOString();
281
+ }
282
+
283
+ private distributeActionDates(start: Date, end: Date, count: number): Date[] {
284
+ if (count === 0) return [];
285
+
286
+ const totalTime = end.getTime() - start.getTime();
287
+ const interval = totalTime / (count + 1);
288
+
289
+ const dates: Date[] = [];
290
+ for (let i = 1; i <= count; i++) {
291
+ dates.push(new Date(start.getTime() + interval * i));
292
+ }
293
+
294
+ return dates;
295
+ }
296
+ }
297
+
298
+ export const stcMapper = new StcMapper();
@@ -0,0 +1,7 @@
1
+ // coaiajs/src/pipeline/index.ts — Barrel export
2
+
3
+ export {
4
+ TemplateLoader,
5
+ TemplateRenderer,
6
+ MobileTemplateEngine,
7
+ } from './template-engine.js';
@@ -0,0 +1,398 @@
1
+ // coaiajs/src/pipeline/template-engine.ts — Pipeline template engine
2
+ // Port of coaiapy's pipeline.py + mobile_template.py
3
+
4
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
5
+ import { join, extname } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { v4 as uuidv4 } from 'uuid';
8
+ import * as yaml from 'js-yaml';
9
+ import type { PipelineTemplate, PipelineStep, PipelineVariable } from '../types.js';
10
+
11
+ // ─── MobileTemplateEngine ───────────────────────────────────────────
12
+ // Lightweight template engine replacing Jinja2 for TypeScript
13
+
14
+ const VARIABLE_RE = /\{\{([^}]+)\}\}/g;
15
+ const CONDITION_RE = /\{%\s*if\s+([^%]+)%\}(.*?)\{%\s*endif\s*%\}/gs;
16
+
17
+ type BuiltinFn = () => string;
18
+
19
+ const BUILTIN_FUNCTIONS: Record<string, BuiltinFn> = {
20
+ uuid4: () => uuidv4(),
21
+ now: () => new Date().toISOString(),
22
+ timestamp: () => new Date().toISOString(),
23
+ mobile_id: () => `mobile_${uuidv4().slice(0, 8)}`,
24
+ touch_timestamp: () => new Date().toLocaleTimeString('en-US', { hour12: false }),
25
+ };
26
+
27
+ export class MobileTemplateEngine {
28
+ renderString(text: string, variables: Record<string, string>): string {
29
+ if (!text) return '';
30
+ let result = processConditionals(text, variables);
31
+ result = processVariables(result, variables);
32
+ return result;
33
+ }
34
+
35
+ renderDict(
36
+ data: Record<string, unknown>,
37
+ variables: Record<string, string>,
38
+ ): Record<string, unknown> {
39
+ if (!data || typeof data !== 'object') return data;
40
+
41
+ const result: Record<string, unknown> = {};
42
+ for (const [key, value] of Object.entries(data)) {
43
+ if (typeof value === 'string') {
44
+ result[key] = this.renderString(value, variables);
45
+ } else if (Array.isArray(value)) {
46
+ result[key] = this.renderList(value, variables);
47
+ } else if (value !== null && typeof value === 'object') {
48
+ result[key] = this.renderDict(value as Record<string, unknown>, variables);
49
+ } else {
50
+ result[key] = value;
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+
56
+ private renderList(data: unknown[], variables: Record<string, string>): unknown[] {
57
+ return data.map((item) => {
58
+ if (typeof item === 'string') return this.renderString(item, variables);
59
+ if (Array.isArray(item)) return this.renderList(item, variables);
60
+ if (item !== null && typeof item === 'object') {
61
+ return this.renderDict(item as Record<string, unknown>, variables);
62
+ }
63
+ return item;
64
+ });
65
+ }
66
+ }
67
+
68
+ // ─── Template Processing ────────────────────────────────────────────
69
+
70
+ function processVariables(content: string, variables: Record<string, string>): string {
71
+ return content.replace(VARIABLE_RE, (_match, expr: string) => {
72
+ const trimmed = expr.trim();
73
+
74
+ // Function calls: uuid4(), now(), etc.
75
+ if (trimmed.endsWith('()')) {
76
+ const fnName = trimmed.slice(0, -2);
77
+ if (fnName in BUILTIN_FUNCTIONS) return BUILTIN_FUNCTIONS[fnName]();
78
+ }
79
+
80
+ // Filters: variable|title, variable|upper
81
+ if (trimmed.includes('|')) {
82
+ const [varName, filterName] = trimmed.split('|').map((s) => s.trim());
83
+ const val = variables[varName];
84
+ if (val !== undefined) return applyFilter(val, filterName);
85
+ return `[${varName}]`;
86
+ }
87
+
88
+ // "variable or default" expressions
89
+ if (trimmed.includes(' or ')) {
90
+ const parts = trimmed.split(' or ').map((p) => p.trim());
91
+ for (const part of parts) {
92
+ const unquoted = part.replace(/^['"]|['"]$/g, '');
93
+ if (part in variables && variables[part] != null) return variables[part];
94
+ if (part.startsWith("'") || part.startsWith('"')) return unquoted;
95
+ }
96
+ return parts[parts.length - 1].replace(/^['"]|['"]$/g, '');
97
+ }
98
+
99
+ // Simple lookup
100
+ if (trimmed in variables) return variables[trimmed] ?? '';
101
+ return `[${trimmed}]`;
102
+ });
103
+ }
104
+
105
+ function processConditionals(content: string, variables: Record<string, string>): string {
106
+ return content.replace(CONDITION_RE, (_match, condition: string, body: string) => {
107
+ return evaluateCondition(condition.trim(), variables) ? body : '';
108
+ });
109
+ }
110
+
111
+ function evaluateCondition(condition: string, variables: Record<string, string>): boolean {
112
+ if (condition.startsWith('not ')) {
113
+ const varName = condition.slice(4).trim();
114
+ return !isTruthy(variables[varName]);
115
+ }
116
+ if (condition.includes('==')) {
117
+ const [left, right] = condition.split('==').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
118
+ return String(variables[left] ?? left) === right;
119
+ }
120
+ if (condition.includes('!=')) {
121
+ const [left, right] = condition.split('!=').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
122
+ return String(variables[left] ?? left) !== right;
123
+ }
124
+ return isTruthy(variables[condition]);
125
+ }
126
+
127
+ function isTruthy(value: unknown): boolean {
128
+ if (value == null || value === '' || (Array.isArray(value) && value.length === 0)) return false;
129
+ if (typeof value === 'boolean') return value;
130
+ if (typeof value === 'string') return !['false', '0', 'no', 'off', 'disabled'].includes(value.toLowerCase());
131
+ return Boolean(value);
132
+ }
133
+
134
+ function applyFilter(value: string, filter: string): string {
135
+ switch (filter) {
136
+ case 'title': return value.replace(/\b\w/g, (c) => c.toUpperCase());
137
+ case 'upper': return value.toUpperCase();
138
+ case 'lower': return value.toLowerCase();
139
+ case 'capitalize': return value.charAt(0).toUpperCase() + value.slice(1);
140
+ case 'strip': return value.trim();
141
+ default: return value;
142
+ }
143
+ }
144
+
145
+ // ─── TemplateLoader ─────────────────────────────────────────────────
146
+
147
+ export class TemplateLoader {
148
+ private searchPaths: string[];
149
+
150
+ constructor() {
151
+ this.searchPaths = this.getSearchPaths();
152
+ }
153
+
154
+ private getSearchPaths(): string[] {
155
+ const paths: string[] = [];
156
+ const projectPath = join(process.cwd(), '.coaia', 'templates');
157
+ if (existsSync(projectPath)) paths.push(projectPath);
158
+
159
+ const userPath = join(homedir(), '.coaia', 'templates');
160
+ if (existsSync(userPath)) paths.push(userPath);
161
+
162
+ return paths;
163
+ }
164
+
165
+ listTemplates(includePath = false): Array<Record<string, unknown>> {
166
+ const templates: Array<Record<string, unknown>> = [];
167
+ const seen = new Set<string>();
168
+
169
+ for (const searchPath of this.searchPaths) {
170
+ if (!existsSync(searchPath)) continue;
171
+
172
+ for (const file of readdirSync(searchPath)) {
173
+ const ext = extname(file);
174
+ if (ext !== '.json' && ext !== '.yaml' && ext !== '.yml') continue;
175
+
176
+ const stem = file.replace(ext, '');
177
+ if (seen.has(stem)) continue;
178
+
179
+ try {
180
+ const template = this.loadTemplate(stem);
181
+ if (template) {
182
+ const info: Record<string, unknown> = {
183
+ name: template.name,
184
+ description: template.description,
185
+ version: template.version,
186
+ author: template.author,
187
+ };
188
+ if (includePath) info.path = join(searchPath, file);
189
+ templates.push(info);
190
+ seen.add(stem);
191
+ }
192
+ } catch {
193
+ continue;
194
+ }
195
+ }
196
+ }
197
+
198
+ return templates;
199
+ }
200
+
201
+ loadTemplate(name: string): PipelineTemplate | null {
202
+ for (const searchPath of this.searchPaths) {
203
+ // Try JSON
204
+ const jsonFile = join(searchPath, `${name}.json`);
205
+ if (existsSync(jsonFile)) {
206
+ try {
207
+ const data = JSON.parse(readFileSync(jsonFile, 'utf-8'));
208
+ return parsePipelineTemplate(data);
209
+ } catch { continue; }
210
+ }
211
+
212
+ // Try YAML
213
+ for (const ext of ['.yaml', '.yml']) {
214
+ const yamlFile = join(searchPath, `${name}${ext}`);
215
+ if (existsSync(yamlFile)) {
216
+ try {
217
+ const data = yaml.load(readFileSync(yamlFile, 'utf-8')) as Record<string, unknown>;
218
+ return parsePipelineTemplate(data);
219
+ } catch { continue; }
220
+ }
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+
226
+ saveTemplate(template: PipelineTemplate, location: 'user' | 'project' = 'user'): void {
227
+ const savePath = location === 'project'
228
+ ? join(process.cwd(), '.coaia', 'templates')
229
+ : join(homedir(), '.coaia', 'templates');
230
+
231
+ mkdirSync(savePath, { recursive: true });
232
+
233
+ const data = serializePipelineTemplate(template);
234
+ if (!data.createdAt) data.createdAt = new Date().toISOString();
235
+
236
+ const filePath = join(savePath, `${template.name}.json`);
237
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
238
+ }
239
+ }
240
+
241
+ // ─── TemplateRenderer ───────────────────────────────────────────────
242
+
243
+ export class TemplateRenderer {
244
+ private engine = new MobileTemplateEngine();
245
+
246
+ renderTemplate(
247
+ template: PipelineTemplate,
248
+ variables: Record<string, string>,
249
+ ): PipelineStep[] {
250
+ const errors = validateVariables(template, variables);
251
+ if (errors.length) throw new Error(`Template validation failed: ${errors.join('; ')}`);
252
+
253
+ const finalVars = mergeDefaults(template, variables);
254
+ const rendered: PipelineStep[] = [];
255
+
256
+ for (const step of template.steps) {
257
+ if (step.conditional) {
258
+ const expanded = this.engine.renderString(step.conditional, finalVars);
259
+ if (!expanded.trim()) continue;
260
+ }
261
+
262
+ rendered.push({
263
+ name: this.engine.renderString(step.name, finalVars),
264
+ observationType: step.observationType,
265
+ description: step.description ? this.engine.renderString(step.description, finalVars) : undefined,
266
+ parent: step.parent ? this.engine.renderString(step.parent, finalVars) : undefined,
267
+ variables: step.variables
268
+ ? this.engine.renderDict(step.variables as Record<string, unknown>, finalVars) as Record<string, string>
269
+ : undefined,
270
+ metadata: step.metadata
271
+ ? this.engine.renderDict(step.metadata, finalVars) as Record<string, unknown>
272
+ : undefined,
273
+ });
274
+ }
275
+
276
+ return rendered;
277
+ }
278
+
279
+ renderWithJudgeIntegration(
280
+ template: PipelineTemplate,
281
+ variables: Record<string, string>,
282
+ enableJudge = false,
283
+ ): PipelineStep[] {
284
+ const observations = this.renderTemplate(template, variables);
285
+
286
+ if (!enableJudge) return observations;
287
+
288
+ return observations.map((obs) => {
289
+ const name = (obs.name ?? '').toLowerCase();
290
+ const desc = (obs.description ?? '').toLowerCase();
291
+ const indicators = ['judge', 'evaluation', 'assess', 'scoring', 'rating'];
292
+ const isJudge = indicators.some((i) => name.includes(i) || desc.includes(i));
293
+
294
+ if (isJudge) {
295
+ return {
296
+ ...obs,
297
+ metadata: {
298
+ ...obs.metadata,
299
+ judge_integration_ready: true,
300
+ judge_api_placeholder: true,
301
+ },
302
+ };
303
+ }
304
+ return obs;
305
+ });
306
+ }
307
+ }
308
+
309
+ // ─── Helpers ────────────────────────────────────────────────────────
310
+
311
+ function parsePipelineTemplate(data: Record<string, unknown>): PipelineTemplate {
312
+ return {
313
+ name: String(data.name),
314
+ version: String(data.version ?? '1.0'),
315
+ description: String(data.description ?? ''),
316
+ author: data.author as string | undefined,
317
+ createdAt: data.created_at as string | undefined ?? data.createdAt as string | undefined,
318
+ variables: ((data.variables ?? []) as Array<Record<string, unknown>>).map((v) => ({
319
+ name: String(v.name),
320
+ type: String(v.type ?? 'string'),
321
+ required: v.required !== false,
322
+ default: v.default as string | undefined,
323
+ description: v.description as string | undefined,
324
+ choices: v.choices as string[] | undefined,
325
+ })),
326
+ steps: ((data.steps ?? []) as Array<Record<string, unknown>>).map((s) => ({
327
+ name: String(s.name),
328
+ observationType: String(s.observation_type ?? s.observationType ?? 'EVENT'),
329
+ description: s.description as string | undefined,
330
+ parent: s.parent as string | undefined,
331
+ variables: (s.variables ?? {}) as Record<string, string>,
332
+ metadata: (s.metadata ?? {}) as Record<string, unknown>,
333
+ conditional: s.conditional as string | undefined,
334
+ })),
335
+ extends: data.extends as string | undefined,
336
+ metadata: (data.metadata ?? {}) as Record<string, unknown>,
337
+ };
338
+ }
339
+
340
+ function serializePipelineTemplate(t: PipelineTemplate): Record<string, unknown> {
341
+ return {
342
+ name: t.name,
343
+ version: t.version,
344
+ description: t.description,
345
+ author: t.author,
346
+ createdAt: t.createdAt,
347
+ extends: t.extends,
348
+ metadata: t.metadata,
349
+ variables: (t.variables ?? []).map((v: PipelineVariable) => ({
350
+ name: v.name,
351
+ type: v.type,
352
+ required: v.required,
353
+ default: v.default,
354
+ description: v.description,
355
+ choices: v.choices,
356
+ })),
357
+ steps: (t.steps ?? []).map((s: PipelineStep) => ({
358
+ name: s.name,
359
+ observation_type: s.observationType,
360
+ description: s.description,
361
+ parent: s.parent,
362
+ variables: s.variables,
363
+ metadata: s.metadata,
364
+ conditional: s.conditional,
365
+ })),
366
+ };
367
+ }
368
+
369
+ function validateVariables(
370
+ template: PipelineTemplate,
371
+ provided: Record<string, string>,
372
+ ): string[] {
373
+ const errors: string[] = [];
374
+ for (const v of template.variables ?? []) {
375
+ if (v.required && v.default == null && !(v.name in provided)) {
376
+ errors.push(`Required variable '${v.name}' not provided`);
377
+ }
378
+ if (v.choices && v.name in provided && !v.choices.includes(provided[v.name])) {
379
+ errors.push(`Variable '${v.name}' must be one of: ${v.choices.join(', ')}`);
380
+ }
381
+ }
382
+ return errors;
383
+ }
384
+
385
+ function mergeDefaults(
386
+ template: PipelineTemplate,
387
+ provided: Record<string, string>,
388
+ ): Record<string, string> {
389
+ const result: Record<string, string> = {};
390
+ for (const v of template.variables ?? []) {
391
+ if (v.name in provided) {
392
+ result[v.name] = provided[v.name];
393
+ } else if (v.default != null) {
394
+ result[v.name] = String(v.default);
395
+ }
396
+ }
397
+ return result;
398
+ }