terse-cli 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 (118) hide show
  1. package/dist/api.d.ts +3 -0
  2. package/dist/api.d.ts.map +1 -0
  3. package/dist/api.js +50 -0
  4. package/dist/api.js.map +1 -0
  5. package/dist/assertProjectRoot.d.ts +6 -0
  6. package/dist/assertProjectRoot.d.ts.map +1 -0
  7. package/dist/assertProjectRoot.js +19 -0
  8. package/dist/assertProjectRoot.js.map +1 -0
  9. package/dist/codegen.d.ts +120 -0
  10. package/dist/codegen.d.ts.map +1 -0
  11. package/dist/codegen.js +786 -0
  12. package/dist/codegen.js.map +1 -0
  13. package/dist/deploy.d.ts +2 -0
  14. package/dist/deploy.d.ts.map +1 -0
  15. package/dist/deploy.js +124 -0
  16. package/dist/deploy.js.map +1 -0
  17. package/dist/generate.d.ts +2 -0
  18. package/dist/generate.d.ts.map +1 -0
  19. package/dist/generate.js +284 -0
  20. package/dist/generate.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +57 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/init.d.ts +2 -0
  26. package/dist/init.d.ts.map +1 -0
  27. package/dist/init.js +150 -0
  28. package/dist/init.js.map +1 -0
  29. package/dist/integrate.d.ts +2 -0
  30. package/dist/integrate.d.ts.map +1 -0
  31. package/dist/integrate.js +34 -0
  32. package/dist/integrate.js.map +1 -0
  33. package/dist/loadJob.d.ts +10 -0
  34. package/dist/loadJob.d.ts.map +1 -0
  35. package/dist/loadJob.js +86 -0
  36. package/dist/loadJob.js.map +1 -0
  37. package/dist/run.d.ts +12 -0
  38. package/dist/run.d.ts.map +1 -0
  39. package/dist/run.js +164 -0
  40. package/dist/run.js.map +1 -0
  41. package/dist/shared/ApiRoutes.d.ts +274 -0
  42. package/dist/shared/ApiRoutes.d.ts.map +1 -0
  43. package/dist/shared/ApiRoutes.js +269 -0
  44. package/dist/shared/ApiRoutes.js.map +1 -0
  45. package/dist/shared/ClientBoundTools.d.ts +37 -0
  46. package/dist/shared/ClientBoundTools.d.ts.map +1 -0
  47. package/dist/shared/ClientBoundTools.js +37 -0
  48. package/dist/shared/ClientBoundTools.js.map +1 -0
  49. package/dist/shared/Configs.d.ts +425 -0
  50. package/dist/shared/Configs.d.ts.map +1 -0
  51. package/dist/shared/Configs.js +703 -0
  52. package/dist/shared/Configs.js.map +1 -0
  53. package/dist/shared/DonatedState.d.ts +5 -0
  54. package/dist/shared/DonatedState.d.ts.map +1 -0
  55. package/dist/shared/DonatedState.js +13 -0
  56. package/dist/shared/DonatedState.js.map +1 -0
  57. package/dist/shared/Entities.d.ts +8 -0
  58. package/dist/shared/Entities.d.ts.map +1 -0
  59. package/dist/shared/Entities.js +9 -0
  60. package/dist/shared/Entities.js.map +1 -0
  61. package/dist/shared/FrontendRoutes.d.ts +52 -0
  62. package/dist/shared/FrontendRoutes.d.ts.map +1 -0
  63. package/dist/shared/FrontendRoutes.js +52 -0
  64. package/dist/shared/FrontendRoutes.js.map +1 -0
  65. package/dist/shared/Integrations.d.ts +218 -0
  66. package/dist/shared/Integrations.d.ts.map +1 -0
  67. package/dist/shared/Integrations.js +134 -0
  68. package/dist/shared/Integrations.js.map +1 -0
  69. package/dist/shared/InvalidationKeys.d.ts +45 -0
  70. package/dist/shared/InvalidationKeys.d.ts.map +1 -0
  71. package/dist/shared/InvalidationKeys.js +150 -0
  72. package/dist/shared/InvalidationKeys.js.map +1 -0
  73. package/dist/shared/ModelEvents.d.ts +166 -0
  74. package/dist/shared/ModelEvents.d.ts.map +1 -0
  75. package/dist/shared/ModelEvents.js +14 -0
  76. package/dist/shared/ModelEvents.js.map +1 -0
  77. package/dist/shared/Notifications.d.ts +32 -0
  78. package/dist/shared/Notifications.d.ts.map +1 -0
  79. package/dist/shared/Notifications.js +6 -0
  80. package/dist/shared/Notifications.js.map +1 -0
  81. package/dist/shared/RunHistoryTypes.d.ts +94 -0
  82. package/dist/shared/RunHistoryTypes.d.ts.map +1 -0
  83. package/dist/shared/RunHistoryTypes.js +10 -0
  84. package/dist/shared/RunHistoryTypes.js.map +1 -0
  85. package/dist/shared/SocketEvents.d.ts +49 -0
  86. package/dist/shared/SocketEvents.d.ts.map +1 -0
  87. package/dist/shared/SocketEvents.js +53 -0
  88. package/dist/shared/SocketEvents.js.map +1 -0
  89. package/dist/shared/Survey.d.ts +10 -0
  90. package/dist/shared/Survey.d.ts.map +1 -0
  91. package/dist/shared/Survey.js +2 -0
  92. package/dist/shared/Survey.js.map +1 -0
  93. package/dist/shared/TicketSystem.d.ts +154 -0
  94. package/dist/shared/TicketSystem.d.ts.map +1 -0
  95. package/dist/shared/TicketSystem.js +21 -0
  96. package/dist/shared/TicketSystem.js.map +1 -0
  97. package/dist/shared/ToolsTypes.d.ts +18 -0
  98. package/dist/shared/ToolsTypes.d.ts.map +1 -0
  99. package/dist/shared/ToolsTypes.js +2 -0
  100. package/dist/shared/ToolsTypes.js.map +1 -0
  101. package/dist/shared/types.d.ts +744 -0
  102. package/dist/shared/types.d.ts.map +1 -0
  103. package/dist/shared/types.js +16 -0
  104. package/dist/shared/types.js.map +1 -0
  105. package/dist/test.d.ts +2 -0
  106. package/dist/test.d.ts.map +1 -0
  107. package/dist/test.js +62 -0
  108. package/dist/test.js.map +1 -0
  109. package/dist/util.d.ts +12 -0
  110. package/dist/util.d.ts.map +1 -0
  111. package/dist/util.js +20 -0
  112. package/dist/util.js.map +1 -0
  113. package/package.json +35 -0
  114. package/templates/env.example.tmpl +1 -0
  115. package/templates/gitignore.tmpl +3 -0
  116. package/templates/package.json.tmpl +15 -0
  117. package/templates/src/index.ts.tmpl +28 -0
  118. package/templates/tsconfig.json.tmpl +19 -0
@@ -0,0 +1,786 @@
1
+ /**
2
+ * Pure codegen function: integration data → TypeScript source text.
3
+ * No I/O — all side effects live in generate.ts.
4
+ */
5
+ const EMPTY_SECTION = { code: "", imports: new Set() };
6
+ // ── Helpers ───────────────────────────────────────────────────────────
7
+ function toPascalCase(s) {
8
+ return s
9
+ .replace(/[^a-zA-Z0-9]+/g, " ")
10
+ .trim()
11
+ .split(/\s+/)
12
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
13
+ .join("");
14
+ }
15
+ function sectionHeader(name) {
16
+ const dashes = "─".repeat(Math.max(1, 58 - name.length));
17
+ return `// ── ${name} ${dashes}`;
18
+ }
19
+ function escapeString(s) {
20
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
21
+ }
22
+ function suffix(instances, i) {
23
+ return instances.length > 1 ? toPascalCase(instances[i].displayName || `Instance${i + 1}`) : "";
24
+ }
25
+ /** Map a tool's integration key back to the IntegrationType enum value used by ConfigInstance. */
26
+ function toolIntegrationToIntegrationType(toolIntegration) {
27
+ switch (toolIntegration) {
28
+ case "jira":
29
+ case "confluence":
30
+ return "atlassian";
31
+ default:
32
+ return toolIntegration;
33
+ }
34
+ }
35
+ function generateResourceClass(className, fields, staticNameField, items) {
36
+ const lines = [];
37
+ lines.push(`export class ${className} {`);
38
+ const ctorParams = fields.map(f => `public readonly ${f.classField}: ${f.type}`).join(", ");
39
+ lines.push(` constructor(${ctorParams}) {}`);
40
+ if (items.length > 0) {
41
+ lines.push("");
42
+ const usedNames = new Set();
43
+ for (const item of items) {
44
+ let name = toPascalCase(String(item[staticNameField] || "Unknown"));
45
+ if (!name || /^\d/.test(name))
46
+ name = `_${name}`;
47
+ while (usedNames.has(name))
48
+ name += "_";
49
+ usedNames.add(name);
50
+ const args = fields.map(f => {
51
+ const val = item[f.sourceField];
52
+ if (typeof val === "number")
53
+ return String(val);
54
+ return `"${escapeString(String(val ?? ""))}"`;
55
+ }).join(", ");
56
+ lines.push(` static ${name} = new ${className}(${args})`);
57
+ }
58
+ }
59
+ lines.push("}");
60
+ return lines.join("\n");
61
+ }
62
+ // ── GitHub ────────────────────────────────────────────────────────────
63
+ function generateGitHubSection(instances) {
64
+ if (instances.length === 0)
65
+ return EMPTY_SECTION;
66
+ const imports = new Set(["GitHubConfig"]);
67
+ const parts = [sectionHeader("GitHub"), ""];
68
+ for (let i = 0; i < instances.length; i++) {
69
+ const inst = instances[i];
70
+ const sfx = instances.length > 1
71
+ ? toPascalCase(inst.integration.account_name || `Instance${i + 1}`)
72
+ : "";
73
+ const ownerClass = `GithubOwner${sfx}`;
74
+ const repoClass = `Repository${sfx}`;
75
+ const id = inst.integration.id;
76
+ const toStaticName = (raw, fallback) => {
77
+ let name = toPascalCase(raw || fallback);
78
+ if (!name)
79
+ name = fallback;
80
+ if (/^\d/.test(name))
81
+ name = `_${name}`;
82
+ return name;
83
+ };
84
+ const repositoriesWithFullName = inst.repositories.map(repo => {
85
+ const owner = repo.owner || "UnknownOwner";
86
+ const fullName = repo.owner && repo.name ? `${repo.owner}/${repo.name}` : repo.name;
87
+ return { ...repo, owner, fullName };
88
+ });
89
+ const ownerEntries = new Map();
90
+ const usedOwnerNames = new Set();
91
+ for (const repo of repositoriesWithFullName) {
92
+ if (!ownerEntries.has(repo.owner)) {
93
+ let ownerStaticName = toStaticName(repo.owner, "UnknownOwner");
94
+ while (usedOwnerNames.has(ownerStaticName))
95
+ ownerStaticName += "_";
96
+ usedOwnerNames.add(ownerStaticName);
97
+ ownerEntries.set(repo.owner, { staticName: ownerStaticName, repos: [] });
98
+ }
99
+ ownerEntries.get(repo.owner).repos.push(repo);
100
+ }
101
+ parts.push(`export class ${ownerClass} {`);
102
+ parts.push(` constructor(public readonly name: string) {}`);
103
+ if (ownerEntries.size > 0) {
104
+ parts.push("");
105
+ for (const [owner, data] of ownerEntries) {
106
+ parts.push(` static ${data.staticName} = new ${ownerClass}("${escapeString(owner)}")`);
107
+ }
108
+ }
109
+ parts.push("}");
110
+ parts.push("");
111
+ parts.push(`export class ${repoClass} {`);
112
+ parts.push(` constructor(`);
113
+ parts.push(` public readonly repositoryId: number,`);
114
+ parts.push(` public readonly name: string,`);
115
+ parts.push(` public readonly owner: ${ownerClass},`);
116
+ parts.push(` public readonly fullName: string`);
117
+ parts.push(` ) {}`);
118
+ if (ownerEntries.size > 0) {
119
+ parts.push("");
120
+ for (const [owner, data] of ownerEntries) {
121
+ const usedRepoNames = new Set();
122
+ parts.push(` static ${data.staticName} = {`);
123
+ for (const repo of data.repos) {
124
+ let repoStaticName = toStaticName(repo.name, "Repository");
125
+ while (usedRepoNames.has(repoStaticName))
126
+ repoStaticName += "_";
127
+ usedRepoNames.add(repoStaticName);
128
+ parts.push(` ${repoStaticName}: new ${repoClass}(${repo.id}, "${escapeString(repo.name)}", ${ownerClass}.${data.staticName}, "${escapeString(repo.fullName)}"),`);
129
+ }
130
+ parts.push(" } as const");
131
+ }
132
+ }
133
+ parts.push("}");
134
+ parts.push("");
135
+ parts.push(`/** Use in \`triggers[]\` */`);
136
+ parts.push(`export class GithubTrigger${sfx} extends GitHubConfig {`);
137
+ parts.push(` constructor(repositories: ${repoClass}[]) {`);
138
+ parts.push(` super("${id}", repositories.map(r => r.repositoryId))`);
139
+ parts.push(" }");
140
+ parts.push("}");
141
+ parts.push("");
142
+ parts.push(`/** Use in \`skills[]\` */`);
143
+ parts.push(`export class GithubSkill${sfx} extends GitHubConfig {`);
144
+ parts.push(` constructor(repositories: ${repoClass}[]) {`);
145
+ parts.push(` super("${id}", repositories.map(r => r.repositoryId))`);
146
+ parts.push(" }");
147
+ parts.push("}");
148
+ parts.push("");
149
+ }
150
+ return { code: parts.join("\n"), imports };
151
+ }
152
+ // ── Gmail ─────────────────────────────────────────────────────────────
153
+ function generateGmailSection(instances) {
154
+ if (instances.length === 0)
155
+ return EMPTY_SECTION;
156
+ const imports = new Set(["GmailConfig", "GmailOutputConfig", "GmailDraftOutputConfig"]);
157
+ const parts = [sectionHeader("Gmail"), ""];
158
+ for (let i = 0; i < instances.length; i++) {
159
+ const sfx = suffix(instances, i);
160
+ const id = instances[i].id;
161
+ parts.push(`/** Use in \`triggers[]\` */`);
162
+ parts.push(`export class GmailTrigger${sfx} extends GmailConfig {`);
163
+ parts.push(` constructor() { super("${id}") }`);
164
+ parts.push("}");
165
+ parts.push("");
166
+ parts.push(`/** Use in \`skills[]\` */`);
167
+ parts.push(`export class GmailSkill${sfx} extends GmailOutputConfig {`);
168
+ parts.push(` constructor() { super("${id}") }`);
169
+ parts.push("}");
170
+ parts.push("");
171
+ parts.push(`/** Use in \`skills[]\` — creates draft emails */`);
172
+ parts.push(`export class GmailDraftSkill${sfx} extends GmailDraftOutputConfig {`);
173
+ parts.push(` constructor() { super("${id}") }`);
174
+ parts.push("}");
175
+ parts.push("");
176
+ }
177
+ return { code: parts.join("\n"), imports };
178
+ }
179
+ // ── Slack ──────────────────────────────────────────────────────────────
180
+ function generateSlackSection(instances) {
181
+ if (instances.length === 0)
182
+ return EMPTY_SECTION;
183
+ const imports = new Set(["SlackConfig", "SlackOutputConfig"]);
184
+ const parts = [sectionHeader("Slack"), ""];
185
+ for (let i = 0; i < instances.length; i++) {
186
+ const inst = instances[i];
187
+ const sfx = suffix(instances, i);
188
+ const channelClass = `SlackChannel${sfx}`;
189
+ parts.push(generateResourceClass(channelClass, [
190
+ { classField: "channelId", type: "string", sourceField: "id" },
191
+ { classField: "name", type: "string", sourceField: "name" },
192
+ ], "name", inst.channels));
193
+ parts.push("");
194
+ parts.push(`/** Use in \`triggers[]\` */`);
195
+ parts.push(`export class SlackTrigger${sfx} extends SlackConfig {`);
196
+ parts.push(` constructor(channel?: ${channelClass}, listenToUserDms?: boolean, userIds?: string[]) {`);
197
+ parts.push(` super("${inst.id}", channel?.channelId, channel?.name, listenToUserDms, userIds)`);
198
+ parts.push(" }");
199
+ parts.push("}");
200
+ parts.push("");
201
+ parts.push(`/** Use in \`skills[]\` */`);
202
+ parts.push(`export class SlackSkill${sfx} extends SlackOutputConfig {`);
203
+ parts.push(` constructor(channel?: ${channelClass}, userIds?: string[], userNames?: string[], listenToUserDms?: boolean) {`);
204
+ parts.push(` super("${inst.id}", channel?.channelId, channel?.name, userIds, userNames, listenToUserDms)`);
205
+ parts.push(" }");
206
+ parts.push("}");
207
+ parts.push("");
208
+ }
209
+ return { code: parts.join("\n"), imports };
210
+ }
211
+ // ── Figma ─────────────────────────────────────────────────────────────
212
+ function generateFigmaSection(instances) {
213
+ if (instances.length === 0)
214
+ return EMPTY_SECTION;
215
+ const imports = new Set(["FigmaConfig"]);
216
+ const parts = [sectionHeader("Figma"), ""];
217
+ for (let i = 0; i < instances.length; i++) {
218
+ const sfx = suffix(instances, i);
219
+ parts.push(`/** Use in \`triggers[]\` */`);
220
+ parts.push(`export class FigmaTrigger${sfx} extends FigmaConfig {`);
221
+ parts.push(` constructor(fileKey: string, fileName: string, teamId: string) {`);
222
+ parts.push(` super("${instances[i].id}", fileKey, fileName, teamId)`);
223
+ parts.push(" }");
224
+ parts.push("}");
225
+ parts.push("");
226
+ }
227
+ return { code: parts.join("\n"), imports };
228
+ }
229
+ // ── Linear ────────────────────────────────────────────────────────────
230
+ function generateLinearSection(instances) {
231
+ if (instances.length === 0)
232
+ return EMPTY_SECTION;
233
+ const imports = new Set(["LinearInputConfig", "LinearOutputConfig"]);
234
+ const parts = [sectionHeader("Linear"), ""];
235
+ for (let i = 0; i < instances.length; i++) {
236
+ const inst = instances[i];
237
+ const sfx = suffix(instances, i);
238
+ const teamClass = `LinearTeam${sfx}`;
239
+ parts.push(generateResourceClass(teamClass, [
240
+ { classField: "teamId", type: "string", sourceField: "id" },
241
+ { classField: "name", type: "string", sourceField: "name" },
242
+ { classField: "key", type: "string", sourceField: "key" },
243
+ ], "name", inst.teams));
244
+ parts.push("");
245
+ parts.push(`/** Use in \`triggers[]\` */`);
246
+ parts.push(`export class LinearTrigger${sfx} extends LinearInputConfig {`);
247
+ parts.push(` constructor(projectId?: string, projectName?: string) {`);
248
+ parts.push(` super("${inst.id}", projectId, projectName)`);
249
+ parts.push(" }");
250
+ parts.push("}");
251
+ parts.push("");
252
+ parts.push(`/** Use in \`skills[]\` */`);
253
+ parts.push(`export class LinearSkill${sfx} extends LinearOutputConfig {`);
254
+ parts.push(` constructor(team?: ${teamClass}, projectId?: string, projectName?: string) {`);
255
+ parts.push(` super("${inst.id}", team?.teamId, team?.name, projectId, projectName)`);
256
+ parts.push(" }");
257
+ parts.push("}");
258
+ parts.push("");
259
+ }
260
+ return { code: parts.join("\n"), imports };
261
+ }
262
+ // ── Jira & Confluence (Atlassian) ─────────────────────────────────────
263
+ function generateAtlassianSection(instances) {
264
+ if (instances.length === 0)
265
+ return EMPTY_SECTION;
266
+ const imports = new Set(["JiraConfig", "ConfluenceConfig"]);
267
+ const parts = [sectionHeader("Jira & Confluence"), ""];
268
+ for (let i = 0; i < instances.length; i++) {
269
+ const inst = instances[i];
270
+ const sfx = suffix(instances, i);
271
+ const projectClass = `JiraProject${sfx}`;
272
+ const pageClass = `ConfluencePage${sfx}`;
273
+ // Jira projects
274
+ parts.push(generateResourceClass(projectClass, [
275
+ { classField: "projectKey", type: "string", sourceField: "key" },
276
+ { classField: "projectId", type: "string", sourceField: "id" },
277
+ { classField: "name", type: "string", sourceField: "name" },
278
+ ], "name", inst.jiraProjects));
279
+ parts.push("");
280
+ parts.push(`/** Use in \`triggers[]\` */`);
281
+ parts.push(`export class JiraTrigger${sfx} extends JiraConfig {`);
282
+ parts.push(` constructor(project?: ${projectClass}) {`);
283
+ parts.push(` super("${inst.id}", project?.projectKey, project?.projectId)`);
284
+ parts.push(" }");
285
+ parts.push("}");
286
+ parts.push("");
287
+ parts.push(`/** Use in \`skills[]\` */`);
288
+ parts.push(`export class JiraSkill${sfx} extends JiraConfig {`);
289
+ parts.push(` constructor(project?: ${projectClass}) {`);
290
+ parts.push(` super("${inst.id}", project?.projectKey, project?.projectId)`);
291
+ parts.push(" }");
292
+ parts.push("}");
293
+ parts.push("");
294
+ // Confluence pages
295
+ parts.push(generateResourceClass(pageClass, [
296
+ { classField: "pageId", type: "string", sourceField: "id" },
297
+ { classField: "title", type: "string", sourceField: "title" },
298
+ { classField: "spaceId", type: "string", sourceField: "spaceId" },
299
+ { classField: "spaceName", type: "string", sourceField: "spaceName" },
300
+ ], "title", inst.confluencePages));
301
+ parts.push("");
302
+ parts.push(`/** Use in \`skills[]\` */`);
303
+ parts.push(`export class ConfluenceSkill${sfx} extends ConfluenceConfig {`);
304
+ parts.push(` constructor(page: ${pageClass}) {`);
305
+ parts.push(` super("${inst.id}", page.spaceName, page.spaceId, page.pageId, page.title)`);
306
+ parts.push(" }");
307
+ parts.push("}");
308
+ parts.push("");
309
+ }
310
+ return { code: parts.join("\n"), imports };
311
+ }
312
+ // ── Notion ────────────────────────────────────────────────────────────
313
+ function generateNotionSection(instances) {
314
+ if (instances.length === 0)
315
+ return EMPTY_SECTION;
316
+ const imports = new Set(["NotionConfig"]);
317
+ const parts = [sectionHeader("Notion"), ""];
318
+ for (let i = 0; i < instances.length; i++) {
319
+ const inst = instances[i];
320
+ const sfx = suffix(instances, i);
321
+ const dbClass = `NotionDatabase${sfx}`;
322
+ const pageClass = `NotionPage${sfx}`;
323
+ parts.push(generateResourceClass(dbClass, [
324
+ { classField: "databaseId", type: "string", sourceField: "id" },
325
+ { classField: "title", type: "string", sourceField: "title" },
326
+ ], "title", inst.databases));
327
+ parts.push("");
328
+ parts.push(generateResourceClass(pageClass, [
329
+ { classField: "pageId", type: "string", sourceField: "id" },
330
+ { classField: "title", type: "string", sourceField: "title" },
331
+ ], "title", inst.pages));
332
+ parts.push("");
333
+ parts.push(`/** Use in \`skills[]\` */`);
334
+ parts.push(`export class NotionSkill${sfx} extends NotionConfig {`);
335
+ parts.push(` constructor(databases?: ${dbClass}[], pages?: ${pageClass}[]) {`);
336
+ parts.push(` super("${inst.id}",`);
337
+ parts.push(` databases?.map(d => d.databaseId), databases?.map(d => d.title),`);
338
+ parts.push(` pages?.map(p => p.pageId), pages?.map(p => p.title))`);
339
+ parts.push(" }");
340
+ parts.push("}");
341
+ parts.push("");
342
+ }
343
+ return { code: parts.join("\n"), imports };
344
+ }
345
+ // ── PostHog ───────────────────────────────────────────────────────────
346
+ function generatePosthogSection(instances) {
347
+ if (instances.length === 0)
348
+ return EMPTY_SECTION;
349
+ const imports = new Set(["PosthogConfig"]);
350
+ const parts = [sectionHeader("PostHog"), ""];
351
+ for (let i = 0; i < instances.length; i++) {
352
+ const inst = instances[i];
353
+ const sfx = suffix(instances, i);
354
+ const projClass = `PosthogProject${sfx}`;
355
+ parts.push(generateResourceClass(projClass, [
356
+ { classField: "projectId", type: "string", sourceField: "id" },
357
+ { classField: "name", type: "string", sourceField: "name" },
358
+ ], "name", inst.projects));
359
+ parts.push("");
360
+ parts.push(`/** Use in \`skills[]\` */`);
361
+ parts.push(`export class PosthogSkill${sfx} extends PosthogConfig {`);
362
+ parts.push(` constructor(project: ${projClass}) {`);
363
+ parts.push(` super("${inst.id}", project.projectId, project.name)`);
364
+ parts.push(" }");
365
+ parts.push("}");
366
+ parts.push("");
367
+ }
368
+ return { code: parts.join("\n"), imports };
369
+ }
370
+ // ── Datadog ───────────────────────────────────────────────────────────
371
+ function generateDatadogSection(instances) {
372
+ if (instances.length === 0)
373
+ return EMPTY_SECTION;
374
+ const imports = new Set(["DatadogConfig"]);
375
+ const parts = [sectionHeader("Datadog"), ""];
376
+ for (let i = 0; i < instances.length; i++) {
377
+ const inst = instances[i];
378
+ const sfx = suffix(instances, i);
379
+ const indexClass = `DatadogIndex${sfx}`;
380
+ parts.push(generateResourceClass(indexClass, [
381
+ { classField: "name", type: "string", sourceField: "name" },
382
+ ], "name", inst.indexes));
383
+ parts.push("");
384
+ parts.push(`/** Use in \`skills[]\` */`);
385
+ parts.push(`export class DatadogSkill${sfx} extends DatadogConfig {`);
386
+ parts.push(` constructor(indexes?: ${indexClass}[]) {`);
387
+ parts.push(` super("${inst.id}", indexes?.map(i => i.name))`);
388
+ parts.push(" }");
389
+ parts.push("}");
390
+ parts.push("");
391
+ }
392
+ return { code: parts.join("\n"), imports };
393
+ }
394
+ // ── LaunchDarkly ──────────────────────────────────────────────────────
395
+ function generateLaunchDarklySection(instances) {
396
+ if (instances.length === 0)
397
+ return EMPTY_SECTION;
398
+ const imports = new Set(["LaunchDarklyConfig"]);
399
+ const parts = [sectionHeader("LaunchDarkly"), ""];
400
+ for (let i = 0; i < instances.length; i++) {
401
+ const inst = instances[i];
402
+ const sfx = suffix(instances, i);
403
+ const projClass = `LaunchDarklyProject${sfx}`;
404
+ parts.push(generateResourceClass(projClass, [
405
+ { classField: "projectKey", type: "string", sourceField: "key" },
406
+ { classField: "name", type: "string", sourceField: "name" },
407
+ ], "name", inst.projects));
408
+ parts.push("");
409
+ parts.push(`/** Use in \`skills[]\` */`);
410
+ parts.push(`export class LaunchDarklySkill${sfx} extends LaunchDarklyConfig {`);
411
+ parts.push(` constructor(project: ${projClass}, environmentKeys: string[]) {`);
412
+ parts.push(` super("${inst.id}", project.projectKey, environmentKeys)`);
413
+ parts.push(" }");
414
+ parts.push("}");
415
+ parts.push("");
416
+ }
417
+ return { code: parts.join("\n"), imports };
418
+ }
419
+ // ── WorkOS ────────────────────────────────────────────────────────────
420
+ function generateWorkOSSection(instances) {
421
+ if (instances.length === 0)
422
+ return EMPTY_SECTION;
423
+ const imports = new Set(["WorkOSInputConfig", "WorkOSOutputConfig"]);
424
+ const parts = [sectionHeader("WorkOS"), ""];
425
+ for (let i = 0; i < instances.length; i++) {
426
+ const sfx = suffix(instances, i);
427
+ const id = instances[i].id;
428
+ parts.push(`/** Use in \`triggers[]\` */`);
429
+ parts.push(`export class WorkOSTrigger${sfx} extends WorkOSInputConfig {`);
430
+ parts.push(` constructor(eventTypes?: string[]) {`);
431
+ parts.push(` super("${id}", eventTypes)`);
432
+ parts.push(" }");
433
+ parts.push("}");
434
+ parts.push("");
435
+ parts.push(`/** Use in \`skills[]\` */`);
436
+ parts.push(`export class WorkOSSkill${sfx} extends WorkOSOutputConfig {`);
437
+ parts.push(` constructor() { super("${id}") }`);
438
+ parts.push("}");
439
+ parts.push("");
440
+ }
441
+ return { code: parts.join("\n"), imports };
442
+ }
443
+ // ── Attio ─────────────────────────────────────────────────────────────
444
+ function generateAttioSection(instances) {
445
+ if (instances.length === 0)
446
+ return EMPTY_SECTION;
447
+ const imports = new Set(["AttioOutputConfig"]);
448
+ const parts = [sectionHeader("Attio"), ""];
449
+ for (let i = 0; i < instances.length; i++) {
450
+ const inst = instances[i];
451
+ const sfx = suffix(instances, i);
452
+ const objClass = `AttioObject${sfx}`;
453
+ parts.push(generateResourceClass(objClass, [
454
+ { classField: "apiSlug", type: "string", sourceField: "api_slug" },
455
+ { classField: "name", type: "string", sourceField: "singular_noun" },
456
+ ], "singular_noun", inst.objects));
457
+ parts.push("");
458
+ parts.push(`/** Use in \`skills[]\` */`);
459
+ parts.push(`export class AttioSkill${sfx} extends AttioOutputConfig {`);
460
+ parts.push(` constructor(object?: ${objClass}) {`);
461
+ parts.push(` super("${inst.id}", object?.apiSlug)`);
462
+ parts.push(" }");
463
+ parts.push("}");
464
+ parts.push("");
465
+ }
466
+ return { code: parts.join("\n"), imports };
467
+ }
468
+ // ── Typed Tools ───────────────────────────────────────────────────────
469
+ /**
470
+ * Converts a displayName like "Create ticket" to camelCase like "createTicket".
471
+ */
472
+ function toCamelCase(s) {
473
+ const pascal = toPascalCase(s);
474
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
475
+ }
476
+ /**
477
+ * Converts a tool name like "linear_create_ticket" to a PascalCase interface name
478
+ * like "LinearCreateTicketParams".
479
+ */
480
+ function toolNameToInterfaceName(name) {
481
+ return name.split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("") + "Params";
482
+ }
483
+ /**
484
+ * Converts a JSON Schema type to a TypeScript type string.
485
+ * Handles nested objects, arrays, enums, anyOf (nullable), and primitives.
486
+ */
487
+ function jsonSchemaToTs(schema, indent) {
488
+ if (!schema)
489
+ return "unknown";
490
+ // Handle enums
491
+ if (schema.enum) {
492
+ return schema.enum.map(v => typeof v === "string" ? `"${escapeString(v)}"` : String(v)).join(" | ");
493
+ }
494
+ // Handle anyOf (commonly used for nullable: [X, { type: "null" }])
495
+ if (schema.anyOf) {
496
+ const nonNull = schema.anyOf.filter(s => s.type !== "null");
497
+ const hasNull = schema.anyOf.some(s => s.type === "null");
498
+ if (nonNull.length === 1 && hasNull) {
499
+ return `${jsonSchemaToTs(nonNull[0], indent)} | null`;
500
+ }
501
+ return schema.anyOf.map(s => jsonSchemaToTs(s, indent)).join(" | ");
502
+ }
503
+ switch (schema.type) {
504
+ case "string":
505
+ return "string";
506
+ case "number":
507
+ case "integer":
508
+ return "number";
509
+ case "boolean":
510
+ return "boolean";
511
+ case "array":
512
+ if (schema.items) {
513
+ const itemType = jsonSchemaToTs(schema.items, indent);
514
+ // Wrap union types in parens for array
515
+ return itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
516
+ }
517
+ return "unknown[]";
518
+ case "object": {
519
+ if (!schema.properties || Object.keys(schema.properties).length === 0) {
520
+ return "Record<string, unknown>";
521
+ }
522
+ const requiredSet = new Set(schema.required || []);
523
+ const pad = " ".repeat(indent);
524
+ const innerPad = " ".repeat(indent + 4);
525
+ const entries = Object.entries(schema.properties).map(([key, prop]) => {
526
+ const optional = requiredSet.has(key) ? "" : "?";
527
+ const tsType = jsonSchemaToTs(prop, indent + 4);
528
+ const desc = prop.description ? ` /** ${prop.description} */\n${innerPad}` : "";
529
+ return `${desc}${key}${optional}: ${tsType}`;
530
+ });
531
+ return `{\n${innerPad}${entries.join(`\n${innerPad}`)}\n${pad}}`;
532
+ }
533
+ default:
534
+ return "unknown";
535
+ }
536
+ }
537
+ function generateToolsSection(tools, input) {
538
+ if (tools.length === 0)
539
+ return EMPTY_SECTION;
540
+ const imports = new Set(["TerseAgent", "ToolOutputByName"]);
541
+ const parts = [sectionHeader("Typed Tools"), ""];
542
+ // Build instance map: integration type → instances with id & displayName
543
+ const instanceMap = new Map();
544
+ instanceMap.set("slack", input.slack.map(s => ({ id: s.id, displayName: s.displayName })));
545
+ instanceMap.set("github", input.github.map(g => ({ id: g.integration.id, displayName: g.integration.account_name || "" })));
546
+ instanceMap.set("gmail", input.gmail.map(g => ({ id: g.id, displayName: g.displayName })));
547
+ instanceMap.set("figma", input.figma.map(f => ({ id: f.id, displayName: f.displayName })));
548
+ instanceMap.set("linear", input.linear.map(l => ({ id: l.id, displayName: l.displayName })));
549
+ instanceMap.set("jira", input.atlassian.map(a => ({ id: a.id, displayName: a.displayName })));
550
+ instanceMap.set("confluence", input.atlassian.map(a => ({ id: a.id, displayName: a.displayName })));
551
+ instanceMap.set("atlassian", input.atlassian.map(a => ({ id: a.id, displayName: a.displayName })));
552
+ instanceMap.set("notion", input.notion.map(n => ({ id: n.id, displayName: n.displayName })));
553
+ instanceMap.set("posthog", input.posthog.map(p => ({ id: p.id, displayName: p.displayName })));
554
+ instanceMap.set("datadog", input.datadog.map(d => ({ id: d.id, displayName: d.displayName })));
555
+ instanceMap.set("launchdarkly", input.launchdarkly.map(l => ({ id: l.id, displayName: l.displayName })));
556
+ instanceMap.set("workos", input.workos.map(w => ({ id: w.id, displayName: w.displayName })));
557
+ instanceMap.set("attio", input.attio.map(a => ({ id: a.id, displayName: a.displayName })));
558
+ // Group tools by integration
559
+ const byIntegration = new Map();
560
+ for (const tool of tools) {
561
+ const key = tool.integration.toLowerCase();
562
+ if (!byIntegration.has(key))
563
+ byIntegration.set(key, []);
564
+ byIntegration.get(key).push(tool);
565
+ }
566
+ // Check if a tool has integrationId that should be auto-filled
567
+ const hasAutoFillId = (tool) => tool.parameters.properties?.integrationId !== undefined;
568
+ // Clone a schema with integrationId removed from properties and required
569
+ const omitIntegrationId = (schema) => {
570
+ const cloned = { ...schema };
571
+ if (cloned.properties) {
572
+ cloned.properties = Object.fromEntries(Object.entries(cloned.properties).filter(([key]) => key !== "integrationId"));
573
+ }
574
+ if (cloned.required) {
575
+ cloned.required = cloned.required.filter(r => r !== "integrationId");
576
+ }
577
+ return cloned;
578
+ };
579
+ // Ensure GitHub repository params are full "owner/repo".
580
+ // Supports:
581
+ // - Repository objects (uses .fullName)
582
+ // - full string "owner/repo" (passthrough)
583
+ // - short repo name string "repo" (resolved via known generated repositories when unique)
584
+ const normalizeGitHubRepositoryParams = (toolName) => {
585
+ switch (toolName) {
586
+ case "readGitHubFile":
587
+ case "listGitHubPullRequests":
588
+ case "listGitHubDirectory":
589
+ case "listGitHubCommits":
590
+ case "summarizeGitHubPullRequestDiff":
591
+ return "{ ...params, repository: __normalizeGitHubRepository((params as any).repository) }";
592
+ case "searchGitHubCode":
593
+ case "grepGitHubCode":
594
+ return "{ ...params, repositoryNames: __normalizeGitHubRepositoryNames((params as any).repositoryNames) }";
595
+ default:
596
+ return "params";
597
+ }
598
+ };
599
+ // Generate per-tool param types (with integrationId omitted where applicable)
600
+ for (const tool of tools) {
601
+ const typeName = toolNameToInterfaceName(tool.name);
602
+ const schema = hasAutoFillId(tool) ? omitIntegrationId(tool.parameters) : tool.parameters;
603
+ const tsType = jsonSchemaToTs(schema, 0);
604
+ if (tool.description) {
605
+ parts.push(`/** ${tool.description} */`);
606
+ }
607
+ // Always use `type` (not `interface`) so params are assignable to Record<string, unknown>
608
+ parts.push(`export type ${typeName} = ${tsType}`);
609
+ parts.push("");
610
+ }
611
+ // Build tool groups: each group has a key, raw integration, tools, and optional baked-in integrationId
612
+ const groups = [];
613
+ for (const [integration, integrationTools] of byIntegration) {
614
+ const needsAutoFill = integrationTools.some(hasAutoFillId);
615
+ if (needsAutoFill) {
616
+ const instances = instanceMap.get(integration) || [];
617
+ if (instances.length === 0)
618
+ continue; // Skip tools with no matching instances
619
+ for (let i = 0; i < instances.length; i++) {
620
+ const sfx = suffix(instances, i);
621
+ groups.push({ key: integration + sfx, integration, tools: integrationTools, integrationId: instances[i].id });
622
+ }
623
+ }
624
+ else {
625
+ groups.push({ key: integration, integration, tools: integrationTools });
626
+ }
627
+ }
628
+ groups.sort((a, b) => a.key.localeCompare(b.key));
629
+ const githubToolsExist = groups.some(g => g.integration === "github");
630
+ if (githubToolsExist) {
631
+ const githubRepoFullNames = Array.from(new Set(input.github.flatMap(g => g.repositories.map(r => (r.owner && r.name ? `${r.owner}/${r.name}` : r.name).trim())).filter(Boolean)));
632
+ const nameToFullName = new Map();
633
+ const ambiguousNames = new Set();
634
+ for (const fullName of githubRepoFullNames) {
635
+ const [, repoName = fullName] = fullName.split("/", 2);
636
+ if (nameToFullName.has(repoName) && nameToFullName.get(repoName) !== fullName) {
637
+ ambiguousNames.add(repoName);
638
+ nameToFullName.delete(repoName);
639
+ }
640
+ else if (!ambiguousNames.has(repoName)) {
641
+ nameToFullName.set(repoName, fullName);
642
+ }
643
+ }
644
+ parts.push(`const __githubRepoNameToFullName = new Map<string, string>([`);
645
+ for (const [name, fullName] of Array.from(nameToFullName.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
646
+ parts.push(` ["${escapeString(name)}", "${escapeString(fullName)}"],`);
647
+ }
648
+ parts.push(`])`);
649
+ parts.push("");
650
+ parts.push(`function __normalizeGitHubRepository(repo: unknown): string {`);
651
+ parts.push(` if (repo && typeof repo === "object" && "fullName" in (repo as Record<string, unknown>)) {`);
652
+ parts.push(` const fullName = (repo as { fullName?: unknown }).fullName`);
653
+ parts.push(` if (typeof fullName === "string" && fullName.length > 0) return fullName`);
654
+ parts.push(` }`);
655
+ parts.push(` if (typeof repo === "string") {`);
656
+ parts.push(` if (repo.includes("/")) return repo`);
657
+ parts.push(` return __githubRepoNameToFullName.get(repo) ?? repo`);
658
+ parts.push(` }`);
659
+ parts.push(` return String(repo ?? "")`);
660
+ parts.push(`}`);
661
+ parts.push("");
662
+ parts.push(`function __normalizeGitHubRepositoryNames(repositories: unknown): string[] {`);
663
+ parts.push(` if (!Array.isArray(repositories)) return []`);
664
+ parts.push(` return repositories.map(repo => __normalizeGitHubRepository(repo))`);
665
+ parts.push(`}`);
666
+ parts.push("");
667
+ }
668
+ // Generate the GeneratedTools type
669
+ parts.push("export type GeneratedTools = {");
670
+ for (const group of groups) {
671
+ parts.push(` ${group.key}: {`);
672
+ for (const tool of group.tools) {
673
+ const methodName = toCamelCase(tool.displayName);
674
+ const paramsType = toolNameToInterfaceName(tool.name);
675
+ if (tool.description) {
676
+ parts.push(` /** ${tool.description} */`);
677
+ }
678
+ parts.push(` ${methodName}(params: ${paramsType}): Promise<ToolOutputByName["${escapeString(tool.name)}"]>`);
679
+ }
680
+ parts.push(" }");
681
+ }
682
+ parts.push("}");
683
+ parts.push("");
684
+ // Module augmentation — adds `tools` to TerseAgent's type
685
+ parts.push('declare module "terse-sdk" {');
686
+ parts.push(" interface TerseAgent {");
687
+ parts.push(" readonly tools: GeneratedTools");
688
+ parts.push(" }");
689
+ parts.push("}");
690
+ parts.push("");
691
+ // Runtime: factory that builds the tools object for a given agent instance,
692
+ // filtered to only include tools whose integration matches the agent's skills.
693
+ parts.push("function createTools(agent: TerseAgent): GeneratedTools {");
694
+ parts.push(" const allowed: Set<string> = new Set(agent.skills.map(s => s.integrationType))");
695
+ parts.push(" return {");
696
+ for (const group of groups) {
697
+ const intType = toolIntegrationToIntegrationType(group.integration);
698
+ parts.push(` ...(allowed.has("${escapeString(intType)}") ? { ${group.key}: {`);
699
+ for (const tool of group.tools) {
700
+ const methodName = toCamelCase(tool.displayName);
701
+ const paramsType = toolNameToInterfaceName(tool.name);
702
+ const normalizedParamsExpr = group.integration === "github" ? normalizeGitHubRepositoryParams(tool.name) : "params";
703
+ if (group.integrationId && hasAutoFillId(tool)) {
704
+ parts.push(` ${methodName}: (params: ${paramsType}) =>`);
705
+ parts.push(` agent.executeTool<ToolOutputByName["${escapeString(tool.name)}"]>("${escapeString(tool.name)}", { ...(${normalizedParamsExpr}), integrationId: "${escapeString(group.integrationId)}" }),`);
706
+ }
707
+ else {
708
+ parts.push(` ${methodName}: (params: ${paramsType}) =>`);
709
+ parts.push(` agent.executeTool<ToolOutputByName["${escapeString(tool.name)}"]>("${escapeString(tool.name)}", ${normalizedParamsExpr}),`);
710
+ }
711
+ }
712
+ parts.push(` } } : {}),`);
713
+ }
714
+ parts.push(" } as GeneratedTools");
715
+ parts.push("}");
716
+ parts.push("");
717
+ // Expose factory on globalThis for CLI (tsx may load a separate module instance)
718
+ parts.push(";(globalThis as any).__terse_createTools = createTools");
719
+ parts.push("");
720
+ return { code: parts.join("\n"), imports };
721
+ }
722
+ // ── System (always included) ──────────────────────────────────────────
723
+ function generateSystemSection() {
724
+ const imports = new Set(["TimeTriggerConfig", "TerseConfig"]);
725
+ const parts = [];
726
+ parts.push(sectionHeader("Schedule"));
727
+ parts.push("");
728
+ parts.push(`/** Use in \`triggers[]\` — run on a cron schedule */`);
729
+ parts.push("export class ScheduleTrigger extends TimeTriggerConfig {");
730
+ parts.push(" constructor(cronExpression: string) {");
731
+ parts.push(" super(cronExpression)");
732
+ parts.push(" }");
733
+ parts.push("}");
734
+ parts.push("");
735
+ parts.push(sectionHeader("Terse"));
736
+ parts.push("");
737
+ parts.push(`/** Use in \`skills[]\` — built-in web search */`);
738
+ parts.push("export class TerseSkill extends TerseConfig {");
739
+ parts.push(" constructor() {");
740
+ parts.push(" super()");
741
+ parts.push(" }");
742
+ parts.push("}");
743
+ parts.push("");
744
+ return { code: parts.join("\n"), imports };
745
+ }
746
+ // ── Main Codegen Entry Point ──────────────────────────────────────────
747
+ export function generateCode(input) {
748
+ const allImports = new Set();
749
+ const sections = [];
750
+ const sectionGenerators = [
751
+ generateGitHubSection(input.github),
752
+ generateGmailSection(input.gmail),
753
+ generateSlackSection(input.slack),
754
+ generateFigmaSection(input.figma),
755
+ generateLinearSection(input.linear),
756
+ generateAtlassianSection(input.atlassian),
757
+ generateNotionSection(input.notion),
758
+ generatePosthogSection(input.posthog),
759
+ generateDatadogSection(input.datadog),
760
+ generateLaunchDarklySection(input.launchdarkly),
761
+ generateWorkOSSection(input.workos),
762
+ generateAttioSection(input.attio),
763
+ generateToolsSection(input.tools, input),
764
+ generateSystemSection(),
765
+ ];
766
+ for (const section of sectionGenerators) {
767
+ if (section.code) {
768
+ sections.push(section.code);
769
+ section.imports.forEach(i => allImports.add(i));
770
+ }
771
+ }
772
+ // Build import statement
773
+ const sortedImports = [...allImports].sort();
774
+ const importLine = sortedImports.length <= 3
775
+ ? `import { ${sortedImports.join(", ")} } from "terse-sdk"`
776
+ : `import {\n ${sortedImports.join(",\n ")},\n} from "terse-sdk"`;
777
+ const header = [
778
+ "// Auto-generated by `terse generate` — do not edit manually",
779
+ "// Re-run `terse generate` to update.",
780
+ "",
781
+ importLine,
782
+ "",
783
+ ];
784
+ return [...header, ...sections].join("\n");
785
+ }
786
+ //# sourceMappingURL=codegen.js.map