terse-cli 0.1.16 → 0.1.18

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 (142) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +6 -9
  4. package/dist/api.js.map +1 -1
  5. package/dist/assertProjectRoot.d.ts +3 -2
  6. package/dist/assertProjectRoot.d.ts.map +1 -1
  7. package/dist/assertProjectRoot.js +6 -9
  8. package/dist/assertProjectRoot.js.map +1 -1
  9. package/dist/auth.d.ts.map +1 -1
  10. package/dist/auth.js +4 -4
  11. package/dist/auth.js.map +1 -1
  12. package/dist/deploy.d.ts +2 -1
  13. package/dist/deploy.d.ts.map +1 -1
  14. package/dist/deploy.js +19 -23
  15. package/dist/deploy.js.map +1 -1
  16. package/dist/generate.d.ts +2 -1
  17. package/dist/generate.d.ts.map +1 -1
  18. package/dist/generate.js +59 -33
  19. package/dist/generate.js.map +1 -1
  20. package/dist/index.js +12 -12
  21. package/dist/index.js.map +1 -1
  22. package/dist/init.d.ts +2 -1
  23. package/dist/init.d.ts.map +1 -1
  24. package/dist/init.js +30 -58
  25. package/dist/init.js.map +1 -1
  26. package/dist/integrate.d.ts.map +1 -1
  27. package/dist/integrate.js +8 -12
  28. package/dist/integrate.js.map +1 -1
  29. package/dist/integrationApi.d.ts +2 -2
  30. package/dist/integrationApi.d.ts.map +1 -1
  31. package/dist/integrationApi.js.map +1 -1
  32. package/dist/loadJob.d.ts +4 -3
  33. package/dist/loadJob.d.ts.map +1 -1
  34. package/dist/loadJob.js +8 -56
  35. package/dist/loadJob.js.map +1 -1
  36. package/dist/providers/LanguageProvider.d.ts +31 -0
  37. package/dist/providers/LanguageProvider.d.ts.map +1 -0
  38. package/dist/providers/LanguageProvider.js +2 -0
  39. package/dist/providers/LanguageProvider.js.map +1 -0
  40. package/dist/providers/codegenTypes.d.ts +119 -0
  41. package/dist/providers/codegenTypes.d.ts.map +1 -0
  42. package/dist/providers/codegenTypes.js +2 -0
  43. package/dist/providers/codegenTypes.js.map +1 -0
  44. package/dist/providers/python/PythonProvider.d.ts +33 -0
  45. package/dist/providers/python/PythonProvider.d.ts.map +1 -0
  46. package/dist/providers/python/PythonProvider.js +362 -0
  47. package/dist/providers/python/PythonProvider.js.map +1 -0
  48. package/dist/providers/python/preparePythonCodegenData.d.ts +89 -0
  49. package/dist/providers/python/preparePythonCodegenData.d.ts.map +1 -0
  50. package/dist/providers/python/preparePythonCodegenData.js +359 -0
  51. package/dist/providers/python/preparePythonCodegenData.js.map +1 -0
  52. package/dist/providers/python/pythonTemplateEngine.d.ts +3 -0
  53. package/dist/providers/python/pythonTemplateEngine.d.ts.map +1 -0
  54. package/dist/providers/python/pythonTemplateEngine.js +30 -0
  55. package/dist/providers/python/pythonTemplateEngine.js.map +1 -0
  56. package/dist/providers/resolveProvider.d.ts +8 -0
  57. package/dist/providers/resolveProvider.d.ts.map +1 -0
  58. package/dist/providers/resolveProvider.js +47 -0
  59. package/dist/providers/resolveProvider.js.map +1 -0
  60. package/dist/providers/shared/sessionStream.d.ts +24 -0
  61. package/dist/providers/shared/sessionStream.d.ts.map +1 -0
  62. package/dist/providers/shared/sessionStream.js +157 -0
  63. package/dist/providers/shared/sessionStream.js.map +1 -0
  64. package/dist/providers/shared/shellUtils.d.ts +13 -0
  65. package/dist/providers/shared/shellUtils.d.ts.map +1 -0
  66. package/dist/providers/shared/shellUtils.js +69 -0
  67. package/dist/providers/shared/shellUtils.js.map +1 -0
  68. package/dist/providers/templateUtils.d.ts +7 -0
  69. package/dist/providers/templateUtils.d.ts.map +1 -0
  70. package/dist/providers/templateUtils.js +30 -0
  71. package/dist/providers/templateUtils.js.map +1 -0
  72. package/dist/providers/typescript/TypeScriptProvider.d.ts +33 -0
  73. package/dist/providers/typescript/TypeScriptProvider.d.ts.map +1 -0
  74. package/dist/providers/typescript/TypeScriptProvider.js +160 -0
  75. package/dist/providers/typescript/TypeScriptProvider.js.map +1 -0
  76. package/dist/providers/typescript/prepareCodegenData.d.ts +144 -0
  77. package/dist/providers/typescript/prepareCodegenData.d.ts.map +1 -0
  78. package/dist/providers/typescript/prepareCodegenData.js +751 -0
  79. package/dist/providers/typescript/prepareCodegenData.js.map +1 -0
  80. package/dist/providers/typescript/templateEngine.d.ts +3 -0
  81. package/dist/providers/typescript/templateEngine.d.ts.map +1 -0
  82. package/dist/providers/typescript/templateEngine.js +37 -0
  83. package/dist/providers/typescript/templateEngine.js.map +1 -0
  84. package/dist/run.d.ts +2 -11
  85. package/dist/run.d.ts.map +1 -1
  86. package/dist/run.js +7 -194
  87. package/dist/run.js.map +1 -1
  88. package/dist/shared/ToolDisplayUtils.d.ts.map +1 -1
  89. package/dist/shared/ToolDisplayUtils.js +13 -1
  90. package/dist/shared/ToolDisplayUtils.js.map +1 -1
  91. package/dist/shared/types.d.ts +912 -107
  92. package/dist/shared/types.d.ts.map +1 -1
  93. package/dist/test.d.ts +2 -1
  94. package/dist/test.d.ts.map +1 -1
  95. package/dist/test.js +12 -12
  96. package/dist/test.js.map +1 -1
  97. package/dist/util.d.ts +1 -1
  98. package/dist/util.d.ts.map +1 -1
  99. package/dist/util.js.map +1 -1
  100. package/package.json +13 -9
  101. package/templates/python/codegen/generated.hbs +7 -0
  102. package/templates/python/codegen/partials/attio-list-objects-method.hbs +8 -0
  103. package/templates/python/codegen/partials/attio-object-types.hbs +23 -0
  104. package/templates/python/codegen/partials/attio-query-records-method.hbs +45 -0
  105. package/templates/python/codegen/partials/attio-section.hbs +110 -0
  106. package/templates/python/codegen/partials/attio-upsert-record-method.hbs +51 -0
  107. package/templates/python/codegen/partials/deterministic-tools-section.hbs +308 -0
  108. package/templates/python/codegen/partials/file-header.hbs +32 -0
  109. package/templates/python/codegen/partials/schedule-section.hbs +9 -0
  110. package/templates/python/codegen/partials/slack-section.hbs +178 -0
  111. package/templates/python/codegen/partials/snowflake-section.hbs +26 -0
  112. package/templates/python/codegen/partials/tool-name-types.hbs +18 -0
  113. package/templates/python/init/.python-version.hbs +1 -0
  114. package/templates/python/init/README.md.hbs +40 -0
  115. package/templates/python/init/gitignore.hbs +3 -0
  116. package/templates/python/init/pyproject.toml.hbs +16 -0
  117. package/templates/python/init/src/main.py.hbs +21 -0
  118. package/templates/typescript/codegen/generated.hbs +17 -0
  119. package/templates/typescript/codegen/partials/atlassian-section.hbs +32 -0
  120. package/templates/typescript/codegen/partials/attio-section.hbs +38 -0
  121. package/templates/typescript/codegen/partials/datadog-section.hbs +11 -0
  122. package/templates/typescript/codegen/partials/figma-section.hbs +13 -0
  123. package/templates/typescript/codegen/partials/github-section.hbs +62 -0
  124. package/templates/typescript/codegen/partials/gmail-section.hbs +21 -0
  125. package/templates/typescript/codegen/partials/header.hbs +12 -0
  126. package/templates/typescript/codegen/partials/launchdarkly-section.hbs +11 -0
  127. package/templates/typescript/codegen/partials/linear-section.hbs +27 -0
  128. package/templates/typescript/codegen/partials/notion-section.hbs +15 -0
  129. package/templates/typescript/codegen/partials/posthog-section.hbs +11 -0
  130. package/templates/typescript/codegen/partials/resource-class.hbs +9 -0
  131. package/templates/typescript/codegen/partials/slack-section.hbs +22 -0
  132. package/templates/typescript/codegen/partials/snowflake-section.hbs +9 -0
  133. package/templates/typescript/codegen/partials/system-section.hbs +18 -0
  134. package/templates/typescript/codegen/partials/tools-section.hbs +69 -0
  135. package/templates/typescript/codegen/partials/workos-section.hbs +53 -0
  136. package/templates/typescript/init/env.example.hbs +1 -0
  137. package/templates/{package.json.tmpl → typescript/init/package.json.hbs} +1 -1
  138. package/templates/{src/index.ts.tmpl → typescript/init/src/index.ts.hbs} +1 -3
  139. /package/templates/{env.example.tmpl → python/init/env.example.hbs} +0 -0
  140. /package/templates/{.claude/settings.json.tmpl → typescript/init/.claude/settings.json.hbs} +0 -0
  141. /package/templates/{gitignore.tmpl → typescript/init/gitignore.hbs} +0 -0
  142. /package/templates/{tsconfig.json.tmpl → typescript/init/tsconfig.json.hbs} +0 -0
@@ -0,0 +1,308 @@
1
+
2
+
3
+ # === Deterministic tools ===
4
+
5
+ {{#if hasAttioTools}}
6
+ _ATTIO_FILTER_ATTRS_BY_OBJECT: dict[str, frozenset[str]] = {
7
+ {{#each attio.objects}}
8
+ {{#if hasAttributes}}
9
+ {{{apiSlugRepr}}}: {{{attrSlugsFrozensetRepr}}},
10
+ {{/if}}
11
+ {{/each}}
12
+ }
13
+
14
+
15
+ def _attio_invalid_filter_error(obj_slug: str, attribute: str | None = None) -> ValueError:
16
+ message = f"Invalid Attio filter for object {obj_slug!r}"
17
+ if attribute is not None:
18
+ message += f" on attribute {attribute!r}"
19
+ message += ": list shorthand is not supported."
20
+ if attribute == 'domains':
21
+ message += ' Use scalar shorthand "useterse.ai" or explicit Attio syntax {"domain": {"$eq": "useterse.ai"}}.'
22
+ else:
23
+ message += " Use a scalar shorthand or explicit Attio operator/object syntax instead."
24
+ return ValueError(message)
25
+
26
+
27
+ def _validate_attio_filter_node(
28
+ obj_slug: str,
29
+ value: object,
30
+ *,
31
+ attr_slugs: frozenset[str],
32
+ current_attribute: str | None = None,
33
+ ) -> None:
34
+ if isinstance(value, dict):
35
+ for raw_key, child in value.items():
36
+ if not isinstance(raw_key, str):
37
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: filter keys must be strings.")
38
+ key = raw_key
39
+ if key in ('$and', '$or'):
40
+ if not isinstance(child, list):
41
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: {key} must be a list.")
42
+ for item in child:
43
+ if not isinstance(item, dict):
44
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: {key} entries must be objects.")
45
+ _validate_attio_filter_node(obj_slug, item, attr_slugs=attr_slugs)
46
+ continue
47
+ if key == '$not':
48
+ if not isinstance(child, dict):
49
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: $not must be an object.")
50
+ _validate_attio_filter_node(obj_slug, child, attr_slugs=attr_slugs)
51
+ continue
52
+ if key == '$in':
53
+ if not isinstance(child, list):
54
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: $in must be a list.")
55
+ continue
56
+ if key == 'path':
57
+ if not isinstance(child, list):
58
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: path must be a list.")
59
+ continue
60
+ if key == 'constraints':
61
+ if not isinstance(child, dict):
62
+ raise ValueError(f"Invalid Attio filter for object {obj_slug!r}: constraints must be an object.")
63
+ _validate_attio_filter_node(
64
+ obj_slug,
65
+ child,
66
+ attr_slugs=attr_slugs,
67
+ current_attribute=current_attribute,
68
+ )
69
+ continue
70
+
71
+ attribute: str | None = key if key in attr_slugs else current_attribute
72
+ if isinstance(child, list):
73
+ raise _attio_invalid_filter_error(obj_slug, attribute)
74
+ _validate_attio_filter_node(
75
+ obj_slug,
76
+ child,
77
+ attr_slugs=attr_slugs,
78
+ current_attribute=attribute,
79
+ )
80
+
81
+
82
+ def _serialize_attio_filter(obj_slug: str, value: object | None) -> str | None:
83
+ if value is None:
84
+ return None
85
+
86
+ parsed = value
87
+ if isinstance(value, str):
88
+ try:
89
+ parsed = json.loads(value)
90
+ except json.JSONDecodeError as exc:
91
+ raise ValueError(f"Attio filters for object {obj_slug!r} must be valid JSON objects.") from exc
92
+
93
+ if not isinstance(parsed, dict):
94
+ raise ValueError(f"Attio filters for object {obj_slug!r} must be objects.")
95
+
96
+ _validate_attio_filter_node(
97
+ obj_slug,
98
+ parsed,
99
+ attr_slugs=_ATTIO_FILTER_ATTRS_BY_OBJECT.get(obj_slug, frozenset()),
100
+ )
101
+ return json.dumps(parsed, separators=(',', ':'), sort_keys=True)
102
+
103
+
104
+ def _serialize_attio_records(records: Sequence[object]) -> str:
105
+ return json.dumps(records, separators=(',', ':'), sort_keys=True)
106
+
107
+
108
+ class _AttioTools:
109
+ def __init__(self, agent: _SdkTerseAgent) -> None:
110
+ self._agent = agent
111
+ {{#each attio.tools}}
112
+ {{#if (eq name "attio_list_objects")}}{{> attio-list-objects-method tool=this attio=../attio}}{{/if}}
113
+ {{#if (eq name "attio_query_records")}}{{> attio-query-records-method tool=this attio=../attio}}{{/if}}
114
+ {{#if (eq name "attio_upsert_record")}}{{> attio-upsert-record-method tool=this attio=../attio}}{{/if}}
115
+ {{/each}}
116
+ {{/if}}
117
+ {{#if hasSlackTools}}
118
+
119
+ def _normalize_slack_channel_id(channel: object) -> str:
120
+ if hasattr(channel, 'channel_id'):
121
+ return str(getattr(channel, 'channel_id'))
122
+ return str(channel)
123
+
124
+
125
+ def _serialize_slack_blocks(blocks: object | None) -> str | None:
126
+ if blocks is None:
127
+ return None
128
+ if isinstance(blocks, str):
129
+ return blocks
130
+ return json.dumps(blocks, separators=(',', ':'))
131
+
132
+
133
+ class _SlackTools:
134
+ def __init__(self, agent: _SdkTerseAgent) -> None:
135
+ self._agent = agent
136
+ {{#each slack.tools}}
137
+ {{#if (eq name "slack_send_message")}}
138
+
139
+ def send_message(
140
+ self,
141
+ *,
142
+ channel: SlackChannel | str,
143
+ message: str,
144
+ thread_ts: str | None = None,
145
+ blocks: object | None = None,
146
+ ) -> SlackSendMessageToolOutput:
147
+ return SlackSendMessageToolOutput.model_validate(
148
+ self._agent.execute_tool(
149
+ {{{nameRepr}}},
150
+ _compact_dict(
151
+ integrationId={{{../slack.instanceIdRepr}}},
152
+ channelId=_normalize_slack_channel_id(channel),
153
+ message=message,
154
+ thread_ts=thread_ts,
155
+ blocks=_serialize_slack_blocks(blocks),
156
+ ),
157
+ )
158
+ )
159
+ {{/if}}
160
+ {{#if (eq name "slack_list_channels")}}
161
+
162
+ def list_channels(
163
+ self,
164
+ *,
165
+ types: str | None = None,
166
+ limit: int | None = None,
167
+ cursor: str | None = None,
168
+ ) -> SlackListChannelsToolOutput:
169
+ return SlackListChannelsToolOutput.model_validate(
170
+ self._agent.execute_tool(
171
+ {{{nameRepr}}},
172
+ _compact_dict(
173
+ integrationId={{{../slack.instanceIdRepr}}},
174
+ types=types,
175
+ limit=limit,
176
+ cursor=cursor,
177
+ ),
178
+ )
179
+ )
180
+ {{/if}}
181
+ {{#if (eq name "slack_list_users")}}
182
+
183
+ def list_users(self, *, query: str | None = None) -> SlackListUsersToolOutput:
184
+ return SlackListUsersToolOutput.model_validate(
185
+ self._agent.execute_tool(
186
+ {{{nameRepr}}},
187
+ _compact_dict(
188
+ integrationId={{{../slack.instanceIdRepr}}},
189
+ query=query,
190
+ ),
191
+ )
192
+ )
193
+ {{/if}}
194
+ {{#if (eq name "slack_read_conversation")}}
195
+
196
+ def read_conversation(
197
+ self,
198
+ *,
199
+ channel: SlackChannel | str,
200
+ limit: int | None = None,
201
+ cursor: str | None = None,
202
+ ) -> SlackReadConversationToolOutput:
203
+ return SlackReadConversationToolOutput.model_validate(
204
+ self._agent.execute_tool(
205
+ {{{nameRepr}}},
206
+ _compact_dict(
207
+ integrationId={{{../slack.instanceIdRepr}}},
208
+ channelId=_normalize_slack_channel_id(channel),
209
+ limit=limit,
210
+ cursor=cursor,
211
+ ),
212
+ )
213
+ )
214
+ {{/if}}
215
+ {{/each}}
216
+ {{/if}}
217
+ {{#if hasSnowflakeTools}}
218
+
219
+ class _SnowflakeTools:
220
+ def __init__(self, agent: _SdkTerseAgent) -> None:
221
+ self._agent = agent
222
+ {{#each snowflake.tools}}
223
+
224
+ def {{methodName}}(self, query: str) -> {{outputModel}}:
225
+ return {{outputModel}}.model_validate(
226
+ self._agent.execute_tool(
227
+ {{{nameRepr}}},
228
+ {'integrationId': {{{../snowflake.instanceIdRepr}}}, 'query': query},
229
+ )
230
+ )
231
+ {{/each}}
232
+ {{/if}}
233
+ {{#if hasAnyDeterministicTools}}
234
+
235
+ class GeneratedTools:
236
+ {{#if hasAttioTools}}
237
+ attio: _AttioTools
238
+ {{/if}}
239
+ {{#if hasSlackTools}}
240
+ slack: _SlackTools
241
+ {{/if}}
242
+ {{#if hasSnowflakeTools}}
243
+ snowflake: _SnowflakeTools
244
+ {{/if}}
245
+
246
+ def __init__(
247
+ self,
248
+ {{#if hasAttioTools}}
249
+ attio: _AttioTools | None = None,
250
+ {{/if}}
251
+ {{#if hasSlackTools}}
252
+ slack: _SlackTools | None = None,
253
+ {{/if}}
254
+ {{#if hasSnowflakeTools}}
255
+ snowflake: _SnowflakeTools | None = None,
256
+ {{/if}}
257
+ ) -> None:
258
+ {{#if hasAttioTools}}
259
+ if attio is not None:
260
+ self.attio = attio
261
+ {{/if}}
262
+ {{#if hasSlackTools}}
263
+ if slack is not None:
264
+ self.slack = slack
265
+ {{/if}}
266
+ {{#if hasSnowflakeTools}}
267
+ if snowflake is not None:
268
+ self.snowflake = snowflake
269
+ {{/if}}
270
+ {{else}}
271
+
272
+ class GeneratedTools:
273
+ pass
274
+ {{/if}}
275
+
276
+
277
+ class TerseAgent(_SdkTerseAgent):
278
+ tools: GeneratedTools
279
+
280
+
281
+ def create_tools(agent: _SdkTerseAgent) -> GeneratedTools:
282
+ manual_tool_configs = getattr(agent, "manual_tool_configs", None) or agent.skills
283
+ allowed = {config.integration_type for config in manual_tool_configs}
284
+ {{#if hasAnyDeterministicTools}}
285
+ return GeneratedTools(
286
+ {{#if hasAttioTools}}
287
+ attio=_AttioTools(agent) if 'attio' in allowed else None,
288
+ {{/if}}
289
+ {{#if hasSlackTools}}
290
+ slack=_SlackTools(agent) if 'slack' in allowed else None,
291
+ {{/if}}
292
+ {{#if hasSnowflakeTools}}
293
+ snowflake=_SnowflakeTools(agent) if 'snowflake' in allowed else None,
294
+ {{/if}}
295
+ )
296
+ {{else}}
297
+ _ = allowed
298
+ return GeneratedTools()
299
+ {{/if}}
300
+
301
+
302
+ def attach_tools(agent: _SdkTerseAgent) -> GeneratedTools:
303
+ tools = create_tools(agent)
304
+ agent.attach_tools(tools)
305
+ return tools
306
+
307
+
308
+ __all__ = {{{exportedNamesRepr}}}
@@ -0,0 +1,32 @@
1
+ """
2
+ Generated by `terse generate`. Do not edit manually.
3
+
4
+ Type checking:
5
+ uv run ty check src/
6
+ """
7
+
8
+ from __future__ import annotations
9
+ {{#if typingImportsLine}}
10
+
11
+ {{typingImportsLine}}
12
+ {{/if}}
13
+ {{#if pydanticImportsLine}}
14
+
15
+ {{pydanticImportsLine}}
16
+ {{/if}}
17
+ {{#if needsJsonImport}}
18
+
19
+ import json
20
+ {{/if}}
21
+
22
+ from terse_sdk import (
23
+ {{#each sdkImports}}
24
+ {{this}},
25
+ {{/each}}
26
+ )
27
+
28
+
29
+ def _compact_dict(**values: object) -> dict[str, object]:
30
+ """Drop None values while preserving falsey config values like False and []."""
31
+
32
+ return {key: value for key, value in values.items() if value is not None}
@@ -0,0 +1,9 @@
1
+
2
+
3
+ # === Schedule ===
4
+
5
+
6
+ class Schedule:
7
+ @staticmethod
8
+ def cron(expression: str) -> TriggerConfig:
9
+ return TriggerConfig(integration_id='system', integration_type='cron_job', config_type='time_trigger', config=_compact_dict(cronExpression=expression))
@@ -0,0 +1,178 @@
1
+ {{#if slack}}
2
+
3
+
4
+ # === Slack ===
5
+
6
+
7
+ class SlackChannel(BaseModel):
8
+ model_config = ConfigDict(frozen=True)
9
+ channel_id: str
10
+ name: str
11
+ {{#if slack.channels.length}}
12
+ {{#each slack.channels}}
13
+ {{staticName}}: ClassVar["SlackChannel"]
14
+ {{/each}}
15
+
16
+ {{#each slack.channels}}
17
+ SlackChannel.{{staticName}} = SlackChannel(channel_id={{{channelIdRepr}}}, name={{{nameRepr}}})
18
+ {{/each}}
19
+ {{/if}}
20
+
21
+
22
+ class Slack:
23
+ @staticmethod
24
+ def on_message(channel: SlackChannel, *, user_ids: Sequence[str] | None = None) -> TriggerConfig:
25
+ return TriggerConfig(
26
+ integration_id={{{slack.instanceIdRepr}}},
27
+ integration_type='slack',
28
+ event_type='message',
29
+ config_type='slack',
30
+ config=_compact_dict(
31
+ channelId=channel.channel_id,
32
+ channelName=channel.name,
33
+ listenToUserDms=False,
34
+ userIds=list(user_ids) if user_ids is not None else None,
35
+ eventTypes=['message'],
36
+ ),
37
+ )
38
+
39
+ @staticmethod
40
+ def on_dm(*, user_ids: Sequence[str] | None = None) -> TriggerConfig:
41
+ return TriggerConfig(
42
+ integration_id={{{slack.instanceIdRepr}}},
43
+ integration_type='slack',
44
+ event_type='message',
45
+ config_type='slack',
46
+ config=_compact_dict(
47
+ listenToUserDms=True,
48
+ userIds=list(user_ids) if user_ids is not None else None,
49
+ eventTypes=['message'],
50
+ ),
51
+ )
52
+
53
+ @staticmethod
54
+ def trigger(
55
+ *,
56
+ channel: SlackChannel | None = None,
57
+ listen_to_user_dms: bool | None = None,
58
+ user_ids: Sequence[str] | None = None,
59
+ event_types: Sequence[SlackEventType] | None = None,
60
+ ) -> TriggerConfig:
61
+ return TriggerConfig(
62
+ integration_id={{{slack.instanceIdRepr}}},
63
+ integration_type='slack',
64
+ config_type='slack',
65
+ config=_compact_dict(
66
+ channelId=channel.channel_id if channel is not None else None,
67
+ channelName=channel.name if channel is not None else None,
68
+ listenToUserDms=listen_to_user_dms,
69
+ userIds=list(user_ids) if user_ids is not None else None,
70
+ eventTypes=list(event_types) if event_types is not None else None,
71
+ ),
72
+ )
73
+
74
+ @staticmethod
75
+ def skill(
76
+ *,
77
+ channel: SlackChannel | None = None,
78
+ user_ids: Sequence[str] | None = None,
79
+ user_names: Sequence[str] | None = None,
80
+ listen_to_user_dms: bool | None = None,
81
+ ) -> SkillConfig[{{slack.skillToolType}}]:
82
+ return SkillConfig(
83
+ integration_id={{{slack.instanceIdRepr}}},
84
+ integration_type='slack',
85
+ config_type='slack_output',
86
+ config=_compact_dict(
87
+ channelId=channel.channel_id if channel is not None else None,
88
+ channelName=channel.name if channel is not None else None,
89
+ userIds=list(user_ids) if user_ids is not None else None,
90
+ userNames=list(user_names) if user_names is not None else None,
91
+ listenToUserDms=listen_to_user_dms,
92
+ ),
93
+ )
94
+ {{else}}
95
+
96
+
97
+ # === Slack ===
98
+
99
+
100
+ class SlackChannel:
101
+ def __init__(self, channel_id: str, name: str) -> None:
102
+ self.channel_id = channel_id
103
+ self.name = name
104
+
105
+
106
+ class Slack:
107
+ @staticmethod
108
+ def on_message(channel: SlackChannel, *, user_ids: list[str] | None = None) -> TriggerConfig:
109
+ return TriggerConfig(
110
+ integration_id='slack_placeholder',
111
+ integration_type='slack',
112
+ event_type='message',
113
+ config_type='slack',
114
+ config=_compact_dict(
115
+ channelId=channel.channel_id,
116
+ channelName=channel.name,
117
+ listenToUserDms=False,
118
+ userIds=list(user_ids) if user_ids is not None else None,
119
+ eventTypes=['message'],
120
+ ),
121
+ )
122
+
123
+ @staticmethod
124
+ def on_dm(*, user_ids: list[str] | None = None) -> TriggerConfig:
125
+ return TriggerConfig(
126
+ integration_id='slack_placeholder',
127
+ integration_type='slack',
128
+ event_type='message',
129
+ config_type='slack',
130
+ config=_compact_dict(
131
+ listenToUserDms=True,
132
+ userIds=list(user_ids) if user_ids is not None else None,
133
+ eventTypes=['message'],
134
+ ),
135
+ )
136
+
137
+ @staticmethod
138
+ def trigger(
139
+ *,
140
+ channel: SlackChannel | None = None,
141
+ listen_to_user_dms: bool | None = None,
142
+ user_ids: list[str] | None = None,
143
+ event_types: list[str] | None = None,
144
+ ) -> TriggerConfig:
145
+ return TriggerConfig(
146
+ integration_id='slack_placeholder',
147
+ integration_type='slack',
148
+ config_type='slack',
149
+ config=_compact_dict(
150
+ channelId=channel.channel_id if channel is not None else None,
151
+ channelName=channel.name if channel is not None else None,
152
+ listenToUserDms=listen_to_user_dms,
153
+ userIds=list(user_ids) if user_ids is not None else None,
154
+ eventTypes=list(event_types) if event_types is not None else None,
155
+ ),
156
+ )
157
+
158
+ @staticmethod
159
+ def skill(
160
+ *,
161
+ channel: SlackChannel | None = None,
162
+ user_ids: list[str] | None = None,
163
+ user_names: list[str] | None = None,
164
+ listen_to_user_dms: bool | None = None,
165
+ ) -> SkillConfig[str]:
166
+ return SkillConfig(
167
+ integration_id='slack_placeholder',
168
+ integration_type='slack',
169
+ config_type='slack_output',
170
+ config=_compact_dict(
171
+ channelId=channel.channel_id if channel is not None else None,
172
+ channelName=channel.name if channel is not None else None,
173
+ userIds=list(user_ids) if user_ids is not None else None,
174
+ userNames=list(user_names) if user_names is not None else None,
175
+ listenToUserDms=listen_to_user_dms,
176
+ ),
177
+ )
178
+ {{/if}}
@@ -0,0 +1,26 @@
1
+ {{#if snowflake}}
2
+
3
+
4
+ # === Snowflake ===
5
+
6
+
7
+ class Snowflake:
8
+ @staticmethod
9
+ def skill() -> SkillConfig[{{snowflake.skillToolType}}]:
10
+ return SkillConfig(integration_id={{{snowflake.instanceIdRepr}}}, integration_type='snowflake', config_type='snowflake_output', config=_compact_dict())
11
+ {{else}}
12
+
13
+
14
+ # === Snowflake ===
15
+
16
+
17
+ class Snowflake:
18
+ @staticmethod
19
+ def skill() -> SkillConfig[str]:
20
+ return SkillConfig(
21
+ integration_id='snowflake_placeholder',
22
+ integration_type='snowflake',
23
+ config_type='snowflake_output',
24
+ config=_compact_dict(),
25
+ )
26
+ {{/if}}
@@ -0,0 +1,18 @@
1
+ {{#if hasAnyToolNameTypes}}
2
+
3
+
4
+ # === Tool name types ===
5
+
6
+ {{#if attioToolNamesLiteral}}
7
+ AttioToolNames = Literal[{{{attioToolNamesLiteral}}}]
8
+ {{/if}}
9
+ {{#if slackToolNamesLiteral}}
10
+ SlackToolNames = Literal[{{{slackToolNamesLiteral}}}]
11
+ {{/if}}
12
+ {{#if snowflakeToolNamesLiteral}}
13
+ SnowflakeToolNames = Literal[{{{snowflakeToolNamesLiteral}}}]
14
+ {{/if}}
15
+ {{#if allToolNamesLiteral}}
16
+ AllToolNames = Literal[{{{allToolNamesLiteral}}}]
17
+ {{/if}}
18
+ {{/if}}
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,40 @@
1
+ # {{projectName}}
2
+
3
+ Project scaffolded by `terse-cli`.
4
+
5
+ ## Prerequisites
6
+
7
+ - Python `>=3.11`
8
+ - [`uv`](https://docs.astral.sh/uv/getting-started/installation/) for dependency management and local commands
9
+
10
+ If `terse init` did not install dependencies automatically, install `uv` and run:
11
+
12
+ ```bash
13
+ uv sync
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ uv sync
20
+ terse test
21
+ terse deploy
22
+ ```
23
+
24
+ Edit `src/main.py` to register jobs with `@app.job(...)`.
25
+
26
+ ## Generated Helpers
27
+
28
+ The starter scaffold registers `Attio.skill()` and `Snowflake.skill()` by default.
29
+
30
+ After you connect Attio, Slack, or Snowflake in Terse, rerun `terse generate` to refresh `src/terse_generated.py` with the real integration IDs, generated resources, and deterministic tool wrappers.
31
+
32
+ Import helpers like `Attio`, `AttioObject`, `Schedule`, `Slack`, `SlackChannel`, `Snowflake`, and `TerseAgent` from `terse_generated`.
33
+
34
+ If a job includes the integration in `triggers=` or `skills=`, it can call deterministic wrappers like:
35
+
36
+ ```python
37
+ agent.tools.snowflake.execute_query(query="select 1")
38
+ ```
39
+
40
+ Use `agent.tools.slack.*` for direct deterministic Slack calls. Use `skills=[Slack.skill(...)]` when you want Slack to be visible to the model during `run()` or `run_and_wait()`.
@@ -0,0 +1,3 @@
1
+ .venv
2
+ __pycache__
3
+ .env
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "{{projectName}}"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.11"
5
+ dependencies = [
6
+ "{{sdkDependency}}",
7
+ ]
8
+
9
+ [tool.uv]
10
+ package = false
11
+
12
+ [dependency-groups]
13
+ dev = ["ty"]
14
+
15
+ [tool.ty.environment]
16
+ python = ".venv"
@@ -0,0 +1,21 @@
1
+ from terse_sdk import CronJobInputEvent, SdkAgentStreamEventFinalOutput, Terse
2
+ from terse_generated import Attio, Schedule, Snowflake, TerseAgent
3
+
4
+ app = Terse()
5
+
6
+ @app.job(
7
+ name="{{projectName}}",
8
+ triggers=[Schedule.cron("0 9 * * 1")],
9
+ skills=[
10
+ Attio.skill(),
11
+ Snowflake.skill(),
12
+ ],
13
+ )
14
+ def run_job(event: CronJobInputEvent, agent: TerseAgent) -> None:
15
+ prompt = (
16
+ "Tell a joke"
17
+ f"Context: {event.formatted_content}"
18
+ )
19
+ for stream_event in agent.run(prompt, event):
20
+ if isinstance(stream_event, SdkAgentStreamEventFinalOutput):
21
+ print(stream_event.final_output)
@@ -0,0 +1,17 @@
1
+ {{> header}}
2
+
3
+ {{#if github}}{{> github-section github}}{{/if~}}
4
+ {{#if gmail}}{{> gmail-section gmail}}{{/if~}}
5
+ {{#if slack}}{{> slack-section slack}}{{/if~}}
6
+ {{#if figma}}{{> figma-section figma}}{{/if~}}
7
+ {{#if linear}}{{> linear-section linear}}{{/if~}}
8
+ {{#if atlassian}}{{> atlassian-section atlassian}}{{/if~}}
9
+ {{#if notion}}{{> notion-section notion}}{{/if~}}
10
+ {{#if posthog}}{{> posthog-section posthog}}{{/if~}}
11
+ {{#if datadog}}{{> datadog-section datadog}}{{/if~}}
12
+ {{#if launchdarkly}}{{> launchdarkly-section launchdarkly}}{{/if~}}
13
+ {{#if workos}}{{> workos-section workos}}{{/if~}}
14
+ {{#if attio}}{{> attio-section attio}}{{/if~}}
15
+ {{#if snowflake}}{{> snowflake-section snowflake}}{{/if~}}
16
+ {{#if tools}}{{> tools-section tools}}{{/if}}
17
+ {{> system-section system}}