@synap-core/workspace-templates 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/define.ts CHANGED
@@ -10,9 +10,39 @@
10
10
  * shape never drifts per call-site.
11
11
  */
12
12
 
13
- import type { TemplateBentoBlock, WorkspaceYaml } from "./types.js";
13
+ import type { TemplateAutomation, TemplateBentoBlock, WorkspaceYaml } from "./types.js";
14
14
  import { getWorkspaceTemplate } from "./templates.js";
15
15
 
16
+ /**
17
+ * A `TemplateAutomation` as the apply door expects it: `flowDefinition`, not
18
+ * the authored `flow` (mirrors `PackagePostWorkspaceBody['automations'][number]`
19
+ * / the Hub `POST /packages/apply` `AutomationSchema`).
20
+ */
21
+ export interface PackageAutomationWire {
22
+ /** Stable template identity; names may be edited without orphaning the flow. */
23
+ key?: string;
24
+ name: string;
25
+ description?: string;
26
+ triggerType: TemplateAutomation['triggerType'];
27
+ triggerConfig?: Record<string, unknown>;
28
+ flowDefinition?: { nodes: unknown[]; edges: unknown[]; precondition?: string };
29
+ status?: TemplateAutomation['status'];
30
+ }
31
+
32
+ function toAutomationWire(
33
+ automations: TemplateAutomation[] | undefined,
34
+ ): PackageAutomationWire[] | undefined {
35
+ return automations?.map((a) => ({
36
+ key: a.key,
37
+ name: a.name,
38
+ description: a.description,
39
+ triggerType: a.triggerType,
40
+ triggerConfig: a.triggerConfig,
41
+ flowDefinition: a.flow,
42
+ status: a.status,
43
+ }));
44
+ }
45
+
16
46
  /** A widget block as the wire expects it: `bentoLayout[]`, keyed by `widgetType`. */
17
47
  export interface BentoWidgetBlockWire {
18
48
  widgetType: string;
@@ -147,6 +177,21 @@ export interface WorkspaceDefinitionInput {
147
177
  * tools/skills/automations silently dropped on that door.
148
178
  */
149
179
  capabilities?: WorkspaceYaml['integrations'];
180
+ /** Playbooks normalized to the workspace-create input contract. */
181
+ playbooks?: Array<Record<string, unknown>>;
182
+ /**
183
+ * Entity-detail action placements — forwarded verbatim to
184
+ * `applyPackagePostWorkspace`, which merges them into
185
+ * `settings.actionPlacements`. The tRPC door (`createFromDefinition`)
186
+ * declares `actionPlacements` in its zod input and carries them through the
187
+ * shared apply door. No shape transform: the authored form IS the wire form.
188
+ */
189
+ actionPlacements?: WorkspaceYaml['actionPlacements'];
190
+ /**
191
+ * Graph-based automations. Deliberately distinct from the legacy
192
+ * `automations` loop/playbook-trigger wire shape accepted by this door.
193
+ */
194
+ flowAutomations?: PackageAutomationWire[];
150
195
  // Workspace-level metadata (flattened from the `workspace` block)
151
196
  workspaceSubtype?: string;
152
197
  workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
@@ -177,6 +222,27 @@ export interface ToDefinitionResult {
177
222
  templateId: string;
178
223
  }
179
224
 
225
+ /**
226
+ * The workspace-create door persists a compact primitive input contract. A
227
+ * template's entity/choice parameter is still represented precisely by its
228
+ * `subjectProfile` or runtime prompt, while the persisted input value is an ID
229
+ * or selected value string. Keep this conversion at the shared template seam
230
+ * so browser creation and package application cannot silently disagree.
231
+ */
232
+ function toWorkspacePlaybooks(playbooks: WorkspaceYaml["playbooks"]): Array<Record<string, unknown>> | undefined {
233
+ return playbooks?.map((playbook) => ({
234
+ ...playbook,
235
+ params: playbook.params?.map((param) => ({
236
+ name: param.name,
237
+ type: param.type === "number" || param.type === "boolean" ? param.type : "string",
238
+ ...(param.defaultValue !== undefined ? { default: param.defaultValue } : {}),
239
+ ...(param.label
240
+ ? { description: param.label }
241
+ : {}),
242
+ })),
243
+ }));
244
+ }
245
+
180
246
  /**
181
247
  * Resolve a template (by slug or YAML object) into the createFromDefinition
182
248
  * payload. Pass `nameOverride` to rename the workspace at creation time.
@@ -228,6 +294,13 @@ export function toWorkspaceDefinition(
228
294
  workspaceCapabilities: ws.capabilities,
229
295
  sourceRoles: ws.sourceRoles,
230
296
  defaultSources: ws.defaultSources,
297
+ // Workspace creation persists its own compact playbook parameter shape.
298
+ // Map authored entity/choice inputs here instead of dropping playbooks.
299
+ playbooks: toWorkspacePlaybooks(tpl.playbooks),
300
+ // Entity-detail action placements — forwarded verbatim; the apply door
301
+ // resolves playbook/automation refs and merges into settings.actionPlacements.
302
+ actionPlacements: tpl.actionPlacements,
303
+ flowAutomations: toAutomationWire(tpl.automations),
231
304
  onboarding: tpl.onboarding,
232
305
  // Template-composition dependencies — forwarded so the tRPC/UI door gets the
233
306
  // SAME deps-first ordering the CLI/Hub door gets via `toPackageDefinition`.
@@ -281,6 +354,9 @@ type WorkspaceDefinitionForwarded =
281
354
  | 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
282
355
  | 'profileEntityBentoTemplates'
283
356
  | 'integrations' // → capabilities (installed via shared applyPackagePostWorkspace door)
357
+ | 'playbooks' // → normalized compact playbook contract
358
+ | 'actionPlacements' // → settings.actionPlacements via applyPackagePostWorkspace
359
+ | 'automations' // → flowAutomations → shared applyPackagePostWorkspace door
284
360
  | 'onboarding'
285
361
  | 'dependencies';
286
362
 
@@ -297,12 +373,7 @@ type WorkspaceDefinitionDropped =
297
373
  // The door DOES accept `extends`, but no template declares it today (0/21).
298
374
  // Forwarding is a no-op until one does — wire it then.
299
375
  | 'extends'
300
- // REAL GAP (agent-fleet, project-management declare playbooks). The door
301
- // accepts `playbooks`, but the shapes disagree: template param `type` allows
302
- // 'text'|'entity'|'choice', the tRPC enum accepts only string|number|boolean
303
- // (and defaultValue↔default, grants: string[]↔{kind,ref}[]). Raw forwarding =
304
- // zod BAD_REQUEST. Needs a param mapper — separate wave, not a silent drop.
305
- | 'playbooks';
376
+ ;
306
377
 
307
378
  type UnclassifiedWorkspaceDefinitionKey = Exclude<
308
379
  keyof WorkspaceYaml,
@@ -367,6 +438,19 @@ export interface PackageDefinitionOutput {
367
438
  layoutConfig?: WorkspaceYaml['layout'];
368
439
  capabilities?: WorkspaceYaml['integrations'];
369
440
  playbooks?: WorkspaceYaml['playbooks'];
441
+ /** Flow-based automations — mapped to the apply door's `flowDefinition` field. */
442
+ automations?: PackageAutomationWire[];
443
+ /**
444
+ * Entity-detail action placements — carried verbatim (authored form IS the
445
+ * wire form). NOTE: the Hub `PackageApplySchema` (synap-backend
446
+ * `hub-protocol/rest/packages.ts`) is a plain `z.object` and does not yet
447
+ * declare `actionPlacements`, so on the CLI/Hub install path this field is
448
+ * STRIPPED before `applyPackagePostWorkspace` sees it. The tRPC
449
+ * `createFromDefinition` door DOES carry it. Named follow-up: add an
450
+ * `ActionPlacementSchema` field to `PackageApplySchema` to close the Hub half
451
+ * (out of the T2 fence — flagged in the handoff).
452
+ */
453
+ actionPlacements?: WorkspaceYaml['actionPlacements'];
370
454
  onboarding?: WorkspaceYaml['onboarding'];
371
455
  /** Template-composition dependencies — resolved by the apply endpoint. */
372
456
  dependencies?: WorkspaceYaml['dependencies'];
@@ -421,6 +505,8 @@ export function toPackageDefinition(
421
505
  layoutConfig: tpl.layout,
422
506
  capabilities: tpl.integrations,
423
507
  playbooks: tpl.playbooks,
508
+ automations: toAutomationWire(tpl.automations),
509
+ actionPlacements: tpl.actionPlacements,
424
510
  onboarding: tpl.onboarding,
425
511
  dependencies: tpl.dependencies,
426
512
  };
@@ -448,6 +534,8 @@ type PackageDefinitionForwarded =
448
534
  | 'layout' // → layoutConfig
449
535
  | 'integrations' // → capabilities
450
536
  | 'playbooks'
537
+ | 'automations' // → mapped to the apply door's flowDefinition wire shape
538
+ | 'actionPlacements' // → carried verbatim (Hub inbound schema decl is a follow-up)
451
539
  | 'onboarding'
452
540
  | 'dependencies';
453
541
 
@@ -381,9 +381,11 @@ playbooks:
381
381
  goalTemplate: >-
382
382
  Research this competitor ({competitor}) and compare them to us. Investigate
383
383
  their {focus}: positioning, strengths, weaknesses, pricing, and recent
384
- moves. Use the provided company context where relevant: {companyContext}.
385
- Produce: {outputTypes}. Update the competitor entity with what you find and
386
- make clear where we win and where we are exposed.
384
+ moves. Use web search for news and public signal, and use the Apify scraper
385
+ for a deep scan of their site (pricing page, changelog, careers page) when
386
+ a URL is known. Use the provided company context where relevant:
387
+ {companyContext}. Produce: {outputTypes}. Update the competitor entity with
388
+ what you find and make clear where we win and where we are exposed.
387
389
  subjectProfile:
388
390
  profileSlug: competitor
389
391
  params:
@@ -398,8 +400,9 @@ playbooks:
398
400
  # starts paid AI runs — the user turns it on when they want the watch.
399
401
  schedule: { cron: "0 9 * * 1", enabled: false }
400
402
  grants:
401
- - Web Search
402
- - Entity Creator
403
+ - exa_search
404
+ - apify_run_actor
405
+ - entity.create
403
406
  status: active
404
407
 
405
408
  # A radar DISCOVERY run: the self-generating half. No subjectProfile → a single
@@ -409,28 +412,32 @@ playbooks:
409
412
  - name: Discover Competitors
410
413
  description: Scan the market for competitors we don't track yet and add them to the ecosystem — the self-generating half of the radar.
411
414
  goalTemplate: >-
412
- Find companies that compete with us in {ourSpace}. Use neural web search
413
- (the exa_search skill with category "company") to surface real, named
414
- companies — direct competitors, adjacent tools that solve the same job a
415
- different way, and emerging players. For each genuinely NEW one (not already
416
- a competitor in our ecosystem), propose it as a `competitor` entity and
417
- ALWAYS include its `website` (its canonical URL) so it de-duplicates against
418
- what we already track — never create a company that already exists. Skip
419
- anything already known. Report what you added and why each one matters.
415
+ Find companies that compete with us in {ourSpace}. Fan out neural web
416
+ searches with the exa_search skill using category "company" — cover direct
417
+ competitors, adjacent tools that solve the same job a different way, and
418
+ emerging players, not just the obvious incumbents. For each genuinely NEW
419
+ one (not already a competitor in our ecosystem), propose it as a
420
+ `competitor` entity and ALWAYS include its `website` (its canonical URL) as
421
+ the dedup key — never create a company whose website already matches one we
422
+ track. Skip anything already known. Report what you added, the search
423
+ angles you covered, and why each new one matters.
420
424
  schedule: { cron: "0 9 * * 1", enabled: false }
421
425
  params:
422
426
  - { name: ourSpace, type: text, label: Our market / category, required: true }
423
427
  executor: is-agent
424
428
  grants:
425
429
  - exa_search
426
- - Entity Creator
430
+ - entity.create
427
431
  status: active
428
432
 
429
- # The Exa search capability powers the discovery radar (find companies we don't
430
- # track yet). Composed pod-wide at provisioning via the existing integrations
431
- # door; the user supplies the Exa API key once on install.
433
+ # Exa powers the discovery radar (find companies we don't track yet); Apify
434
+ # powers the deep-scan half of Research Competitor (scrape a known competitor's
435
+ # site for pricing/changelog/careers signal). Composed pod-wide at provisioning
436
+ # via the existing integrations door; the user supplies each API key once on
437
+ # install — a missing key fails gracefully per-skill, not the whole workspace.
432
438
  integrations:
433
439
  - templateKey: exa
440
+ - templateKey: apify
434
441
 
435
442
  onboarding:
436
443
  goal: >
package/src/index.ts CHANGED
@@ -44,6 +44,8 @@ export type {
44
44
  TemplateOnboardingCollectTarget,
45
45
  TemplatePlaybook,
46
46
  TemplatePlaybookParam,
47
+ TemplateAutomation,
48
+ TemplateActionPlacement,
47
49
  } from "./types.js";
48
50
 
49
51
  // The dependency-kind vocabulary as a VALUE — so consumers (marketplace,
@@ -83,6 +85,19 @@ export type {
83
85
  TemplateUpdateCheck,
84
86
  } from "./catalog.js";
85
87
 
88
+ export {
89
+ layerTemplateGraph,
90
+ bundledEdgesOf,
91
+ } from "./composition-graph.js";
92
+ export type {
93
+ CompositionInstalledEntry,
94
+ CompositionOverlay,
95
+ CompositionEdge,
96
+ CompositionNode,
97
+ CompositionGraph,
98
+ LayerTemplateGraphInput,
99
+ } from "./composition-graph.js";
100
+
86
101
  export {
87
102
  toWorkspaceDefinition,
88
103
  toPackageDefinition,
@@ -0,0 +1,204 @@
1
+ # Meetings & Knowledge workspace template
2
+ # A standalone capture layer for meetings: pull the transcript, summarize it,
3
+ # and turn it into the things that actually matter afterward — notes, tasks,
4
+ # and the people involved. Reuses the pod-wide person/task/note kinds so a
5
+ # meeting's output lands where every other workspace already looks for it.
6
+
7
+ meta:
8
+ slug: meetings-knowledge
9
+ name: Meetings & Knowledge
10
+ description: Import and summarize meetings — transcripts become notes, action items become tasks, and attendees become tracked people.
11
+ icon: video
12
+ color: "#8B5CF6"
13
+ tags: [meetings, notes, tasks, transcripts, calendar]
14
+ isPublic: true
15
+
16
+ workspace:
17
+ name: Meetings & Knowledge
18
+ description: Import and summarize meetings — transcripts become notes, action items become tasks, and attendees become tracked people.
19
+ subtype: meetings-knowledge
20
+ visibility: pod_visible
21
+ capabilities:
22
+ - meetings.import
23
+ - meetings.summaries
24
+ sourceRoles:
25
+ meetings: provider
26
+
27
+ profiles:
28
+ # ── Meeting (owned here) ─────────────────────────────────────────────────
29
+ - slug: meeting
30
+ displayName: Meeting
31
+ icon: video
32
+ color: violet
33
+ description: A meeting — its date, attendees, and what came out of it.
34
+ properties:
35
+ - slug: meetingDate
36
+ label: Date
37
+ valueType: date
38
+ inputType: date
39
+ - slug: platform
40
+ label: Platform
41
+ valueType: string
42
+ inputType: select
43
+ enumValues: [zoom, google-meet, teams, in-person, other]
44
+ - slug: attendees
45
+ label: Attendees
46
+ valueType: array
47
+ inputType: text
48
+ - slug: status
49
+ label: Status
50
+ valueType: string
51
+ inputType: select
52
+ enumValues: [scheduled, completed, processed]
53
+ constraints: { defaultValue: scheduled }
54
+ - slug: summary
55
+ label: Summary
56
+ valueType: string
57
+ inputType: textarea
58
+ - slug: transcriptUrl
59
+ label: Transcript URL
60
+ valueType: string
61
+ inputType: url
62
+
63
+ views:
64
+ - name: Meetings
65
+ slug: meetings
66
+ type: table
67
+ scopeProfileSlug: meeting
68
+ description: Every meeting, its platform, and whether it's been processed.
69
+ config:
70
+ columns: [title, meetingDate, platform, status]
71
+ defaultView: true
72
+ - name: Meetings Calendar
73
+ slug: meetings-calendar
74
+ type: calendar
75
+ scopeProfileSlug: meeting
76
+ description: Meetings on a calendar, keyed on their date.
77
+ - name: Action Items
78
+ slug: meeting-action-items
79
+ type: kanban
80
+ scopeProfileSlug: task
81
+ groupBy: status
82
+ description: Tasks generated from meetings, by status.
83
+ config:
84
+ groupBy: status
85
+
86
+ entityLinks:
87
+ - sourceProfileSlug: meeting
88
+ targetProfileSlug: person
89
+ type: attended_by
90
+ label: Attended by
91
+ - sourceProfileSlug: meeting
92
+ targetProfileSlug: task
93
+ type: generated
94
+ label: Generated
95
+ - sourceProfileSlug: meeting
96
+ targetProfileSlug: note
97
+ type: summarized_in
98
+ label: Summarized in
99
+
100
+ # Playbook — pulls the transcript (Fireflies) and calendar context (Google
101
+ # Calendar), summarizes it, and writes the follow-through: a note, tasks per
102
+ # action item, and any new attendees. Bound to the meeting kind so a captured
103
+ # meeting can offer this as a session.
104
+ playbooks:
105
+ - name: Import & Summarize Meeting
106
+ description: Pull a meeting's transcript, summarize it, and extract action items and attendees into tasks, notes, and people.
107
+ goalTemplate: >-
108
+ Import the meeting {meeting} — pull its transcript from Fireflies and use
109
+ Google Calendar for scheduling/attendee context. Produce: {outputTypes}.
110
+ Create a note with the summary, create a task for each action item, and
111
+ make sure attendees not already tracked are captured as person entities.
112
+ Update the meeting record's summary, transcriptUrl, and status.
113
+ subjectProfile:
114
+ profileSlug: meeting
115
+ params:
116
+ - { name: meeting, type: entity, label: Meeting, required: true }
117
+ - { name: outputTypes, type: text, label: Outputs, defaultValue: "a summary note, action-item tasks, and updated attendee records" }
118
+ executor: is-agent
119
+ grants:
120
+ - fireflies_import_last_meeting
121
+ - fireflies_search
122
+ - calendar_list
123
+ - entity.create
124
+ status: active
125
+
126
+ # Transcript import via Fireflies, calendar/attendee context via Google
127
+ # (nango-google). The user connects each once on install; a missing key fails
128
+ # gracefully per-skill.
129
+ integrations:
130
+ - templateKey: fireflies
131
+ - templateKey: nango-google
132
+
133
+ onboarding:
134
+ goal: >
135
+ Get recent meetings into the pod so summaries, action items, and attendees
136
+ are captured instead of living only in someone's memory of the call.
137
+ framing: |
138
+ THE CHIEF OF STAFF: Act as a sharp chief of staff who turns meetings into
139
+ follow-through. A meeting with no summary and no action items didn't
140
+ happen as far as the rest of the org is concerned.
141
+ expertise:
142
+ starters:
143
+ - >
144
+ Every meeting worth logging needs a "so what" — at least one action item
145
+ or decision. If a founder describes a meeting with no outcome, ask what
146
+ changed because of it.
147
+ - >
148
+ Attendees are people, not names in a transcript — capture them as
149
+ tracked contacts so the next meeting with them has history.
150
+ blindSpots:
151
+ - >
152
+ Recurring meetings (standups, 1:1s) get skipped because they feel
153
+ routine — but their action items are exactly what falls through the
154
+ cracks. Don't let "it's just a standup" excuse skipping capture.
155
+ bar: >
156
+ A handful of recent meetings are logged with a real summary and at least
157
+ one action item each, so the Import & Summarize playbook has something
158
+ concrete to run against.
159
+ collect:
160
+ - profileSlug: meeting
161
+ what: Recent or upcoming meetings worth tracking.
162
+ cardinality: few
163
+ keyFields: [meetingDate, platform]
164
+ openingQuestions:
165
+ - "What meetings do you want tracked — recurring ones, or just the important ones?"
166
+ - "Where do your meetings live — Zoom, Google Meet, somewhere else?"
167
+ - "Do you already record/transcribe them, or should that start now?"
168
+ doneWhen: A few meetings captured with a date and platform, at least one with a summary.
169
+
170
+ app:
171
+ kind: stored
172
+ cellKey: entity-detail
173
+
174
+ layout:
175
+ defaultView: Meetings
176
+ defaultApp: null
177
+ pinnedApps: [dashboard, data]
178
+ sidebarItems:
179
+ - kind: view
180
+ viewName: Meetings
181
+ label: Meetings
182
+ icon: video
183
+ - kind: view
184
+ viewName: Meetings Calendar
185
+ label: Calendar
186
+ icon: calendar
187
+ - kind: view
188
+ viewName: Action Items
189
+ label: Action Items
190
+ icon: check-square
191
+ - kind: app
192
+ appId: data
193
+ label: Data
194
+ icon: database
195
+
196
+ bento:
197
+ viewName: Meetings
198
+ blocks:
199
+ - kind: view
200
+ viewName: Meetings
201
+ pos: { x: 0, y: 0, w: 8, h: 5 }
202
+ - kind: view
203
+ viewName: Action Items
204
+ pos: { x: 8, y: 0, w: 4, h: 5 }