@synap-core/workspace-templates 0.6.1 → 0.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synap-core/workspace-templates",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@9.15.4",
6
6
  "description": "Canonical workspace definitions — YAML source + typed exports for browser, backend, CLI, and marketplace",
package/src/builder.yaml CHANGED
@@ -1258,6 +1258,27 @@ entityLinks:
1258
1258
  type: depends_on
1259
1259
  label: Depends on
1260
1260
 
1261
+ # Playbooks — session templates seeded with the workspace.
1262
+ # `subjectProfile` binds this playbook to the `devplane_feature` kind so the
1263
+ # Capture → Session matcher (playbooks.subject_profile->>'profileSlug') can offer
1264
+ # it when a feature/idea is captured. The captured feature is passed as the
1265
+ # `feature` entity param and substituted into the goalTemplate.
1266
+ playbooks:
1267
+ - name: Evaluate Feature
1268
+ description: Assess a feature — its current state, whether it's worth building, and how it fits the rest of the system.
1269
+ goalTemplate: >-
1270
+ Evaluate this feature ({feature}): where it stands today (its status,
1271
+ progress, and any blockers), whether it is actually useful (the problem it
1272
+ solves and for whom), and how it integrates with the existing apps,
1273
+ packages, services, and environments in this workspace. Finish with a clear
1274
+ recommendation — continue, cut, or reshape — and the reasoning behind it.
1275
+ subjectProfile:
1276
+ profileSlug: devplane_feature
1277
+ params:
1278
+ - { name: feature, type: entity, label: Feature, required: true }
1279
+ executor: is-agent
1280
+ status: active
1281
+
1261
1282
  onboarding:
1262
1283
  goal: Capture the real engineering surface — the apps that are shipped, where their code lives, what runs behind them, where they deploy, and what's in flight — so the workspace mirrors what the team actually builds and runs, not a generic template.
1263
1284
  framing: >
package/src/catalog.ts CHANGED
@@ -101,6 +101,59 @@ export interface MergeCatalogInput {
101
101
  remoteStatus?: RemoteSourceStatus;
102
102
  }
103
103
 
104
+ /**
105
+ * A control-plane package LIST row, as returned by `GET /api/packages` and
106
+ * `GET /api/packages/mine`. This is the raw JSON shape BEFORE it is projected
107
+ * into a `RemoteEntry`. Both list routes STRIP `definition`, so it is absent
108
+ * here (the install path fetches the full row per-slug on demand).
109
+ *
110
+ * `isPublic` is present ONLY on `/mine` rows (`false` ⇒ private). Public browse
111
+ * rows omit it BY CONSTRUCTION (the list route hard-filters `WHERE is_public =
112
+ * true`), so its absence means "public", never "unknown".
113
+ *
114
+ * Consumers used to hand-copy this shape + `rowToRemoteEntry` (browser
115
+ * `useMarketplaceCatalog`, CLI `cp-packages`); this is the ONE canonical copy.
116
+ */
117
+ export interface CpPackageRow {
118
+ slug: string;
119
+ displayName: string;
120
+ description?: string | null;
121
+ /** CP package category / type (e.g. `"workspace"`, `"view"`). */
122
+ category?: string;
123
+ tags?: string[] | null;
124
+ icon?: string | null;
125
+ /** Present only on `/mine` rows. `false` ⇒ private. Browse rows omit it. */
126
+ isPublic?: boolean;
127
+ }
128
+
129
+ /**
130
+ * Map a CP list row into the merge door's `RemoteEntry`. THE canonical mapping —
131
+ * browser + CLI import this instead of hand-copying it (the two copies had
132
+ * already drifted in `description`/`isPrivate` handling).
133
+ *
134
+ * `isPrivate` is passed EXPLICITLY by the caller because it depends on WHICH
135
+ * route the row came from: browse rows are all-public (pass `undefined`), `/mine`
136
+ * rows carry the real flag (pass `row.isPublic === false`). Keeping it a param —
137
+ * not derived here — lets a caller that fetches the two routes separately control
138
+ * it per-source; `assembleCatalog` derives it from the row for the common case.
139
+ */
140
+ export function rowToRemoteEntry(
141
+ row: CpPackageRow,
142
+ isPrivate?: boolean,
143
+ ): RemoteEntry {
144
+ return {
145
+ slug: row.slug,
146
+ name: row.displayName,
147
+ description: row.description ?? undefined,
148
+ category: row.category,
149
+ tags: row.tags ?? [],
150
+ icon: row.icon ?? null,
151
+ isPrivate,
152
+ // Both list routes strip `definition`; absence ⇒ bundle wins a slug conflict,
153
+ // and the install path fetches the full definition per-slug when needed.
154
+ };
155
+ }
156
+
104
157
  // ============================================================================
105
158
  // Output
106
159
  // ============================================================================
@@ -283,3 +336,38 @@ export function mergeCatalog(input: MergeCatalogInput): MergeCatalogResult {
283
336
  sources: { bundled: "ok", remote: remoteStatus },
284
337
  };
285
338
  }
339
+
340
+ export interface AssembleCatalogInput {
341
+ /** Bundled templates (this package). Always in-process. */
342
+ bundled: BundledEntry[];
343
+ /**
344
+ * Raw CP list rows (browse and/or `/mine`, concatenated). Mapped to
345
+ * `RemoteEntry` via `rowToRemoteEntry`; `isPrivate` is DERIVED per-row from
346
+ * `isPublic` (`false` ⇒ private; absent/`true` ⇒ public), which handles both
347
+ * browse rows (no `isPublic`) and `/mine` rows uniformly.
348
+ */
349
+ remoteRows?: CpPackageRow[];
350
+ /** This client's bundled-package version (the winner signal vs each row). */
351
+ bundleVersion: string;
352
+ /** The transport's verdict on the remote fetch. Default `"ok"`. */
353
+ remoteStatus?: RemoteSourceStatus;
354
+ }
355
+
356
+ /**
357
+ * Convenience wrapper over `mergeCatalog`: maps raw CP rows through the canonical
358
+ * `rowToRemoteEntry` and merges them with the bundled templates. Still PURE — the
359
+ * caller fetches the rows (its own transport/auth) and passes them in; this only
360
+ * maps + merges. Use `mergeCatalog` + `rowToRemoteEntry` directly when you need
361
+ * per-source `isPrivate` control (e.g. keeping browse and `/mine` distinct).
362
+ */
363
+ export function assembleCatalog(input: AssembleCatalogInput): MergeCatalogResult {
364
+ const remote = (input.remoteRows ?? []).map((row) =>
365
+ rowToRemoteEntry(row, row.isPublic === false ? true : undefined),
366
+ );
367
+ return mergeCatalog({
368
+ bundled: input.bundled,
369
+ remote,
370
+ bundleVersion: input.bundleVersion,
371
+ remoteStatus: input.remoteStatus,
372
+ });
373
+ }
@@ -402,6 +402,20 @@ views:
402
402
  scopeProfileSlug: post
403
403
  groupBy: post-status
404
404
  description: Content posts by status.
405
+ # The editorial calendar — the "it lands on a calendar" promise. Posts render
406
+ # on a month grid keyed on their Publish Date. The calendar renderer resolves
407
+ # the event date via getProperty(entity, dateField) against the entity's nested
408
+ # `properties` object (CalendarView default is `properties.date`), so the
409
+ # dateField must be the `properties.`-prefixed path, NOT a bare slug — a bare
410
+ # slug only anchors the visible month, it does not map events.
411
+ # `publish-date` is valueType:date, so values render as all-day chips.
412
+ - name: Content Calendar
413
+ slug: content-calendar
414
+ type: calendar
415
+ scopeProfileSlug: post
416
+ description: Your editorial calendar — every post on the day it publishes.
417
+ config:
418
+ dateField: properties.publish-date
405
419
  - name: Pillars
406
420
  slug: content-pillars
407
421
  type: list
@@ -434,6 +448,10 @@ layout:
434
448
  viewName: Posts
435
449
  label: Posts
436
450
  icon: file-edit
451
+ - kind: view
452
+ viewName: Content Calendar
453
+ label: Calendar
454
+ icon: calendar
437
455
  - kind: view
438
456
  viewName: Pillars
439
457
  label: Pillars
@@ -451,12 +469,15 @@ layout:
451
469
  bento:
452
470
  viewName: Posts
453
471
  blocks:
472
+ - kind: view
473
+ viewName: Content Calendar
474
+ pos: { x: 0, y: 0, w: 12, h: 6 }
454
475
  - kind: view
455
476
  viewName: Pillars
456
- pos: { x: 0, y: 0, w: 6, h: 5 }
477
+ pos: { x: 0, y: 6, w: 6, h: 5 }
457
478
  - kind: view
458
479
  viewName: Newsletters
459
- pos: { x: 6, y: 0, w: 6, h: 5 }
480
+ pos: { x: 6, y: 6, w: 6, h: 5 }
460
481
 
461
482
  # Editorial graph: connect posts to campaign / pillar / sponsor
462
483
  entityLinks:
@@ -473,6 +494,28 @@ entityLinks:
473
494
  type: sponsored_by
474
495
  label: Sponsor
475
496
 
497
+ # Playbooks — session templates seeded with the workspace.
498
+ # `subjectProfile` binds this playbook to the `post` kind so the Capture → Session
499
+ # matcher (playbooks.subject_profile->>'profileSlug') can offer it when a post is
500
+ # captured. The captured post is passed as the `post` entity param and substituted
501
+ # into the goalTemplate; the agent carries it from raw idea to a scheduled draft.
502
+ playbooks:
503
+ - name: Produce Content
504
+ description: Take a captured post idea and carry it from spark to a scheduled, platform-ready draft.
505
+ goalTemplate: >-
506
+ Produce content for {platform} from this idea ({post}). Take the raw idea,
507
+ shape it into a {format} draft with a scroll-stopping hook and a clear CTA,
508
+ keep it aligned to the post's pillar and the operator's brand voice, then
509
+ move it to Scheduled with a publish date.
510
+ subjectProfile:
511
+ profileSlug: post
512
+ params:
513
+ - { name: post, type: entity, label: Post, required: true }
514
+ - { name: platform, type: text, label: Platform, required: true }
515
+ - { name: format, type: text, label: Format }
516
+ executor: is-agent
517
+ status: active
518
+
476
519
  onboarding:
477
520
  goal: >
478
521
  Stand up the editorial engine — the pillars you're known for, the platforms
package/src/crm.yaml CHANGED
@@ -1,7 +1,8 @@
1
1
  # CRM workspace template
2
2
  # The CRM owns pre-sale work only. A won deal is handed to the operating
3
3
  # workspace; this template deliberately does not provision client delivery,
4
- # playbooks, campaigns, tasks, documents, or automations.
4
+ # campaigns, tasks, documents, or automations. It DOES seed funnel playbooks
5
+ # (the action layer) so the pipeline is operable via sessions — see `playbooks:`.
5
6
 
6
7
  meta:
7
8
  slug: crm
@@ -405,3 +406,68 @@ entityLinks:
405
406
  - sourceProfileSlug: company
406
407
  targetProfileSlug: deal
407
408
  type: linked_to_deal
409
+
410
+ # Playbooks — session templates seeded with the workspace. The CRM's action layer.
411
+ # `subjectProfile` binds each playbook to a funnel subject so the Capture → Session
412
+ # matcher (`playbooks.matchForEntity`, filtering `subject_profile->>'profileSlug'`)
413
+ # can offer it when that subject is captured — a captured lead surfaces "Enrich this
414
+ # lead" / "Qualify this lead"; a deal surfaces "Work this deal". The subject entity
415
+ # is passed as the matching entity param and substituted into the goalTemplate.
416
+ #
417
+ # Two subjects are used: `lead` (a shared ROLE facet on a person/company — the same
418
+ # facet-role subject pattern ecosystem.yaml uses for `competitor`) and `deal` (a
419
+ # primary CRM kind). The matcher keys purely on the profileSlug string, so a role
420
+ # facet and a kind are matched identically.
421
+ #
422
+ # No `grants:` on "Enrich this lead": enrichment tools (Apify/Unipile-LinkedIn) are
423
+ # workspace-conditional and may not be granted, so the goalTemplate context-injects
424
+ # the intent and the session agent uses whatever enrichment capability the workspace
425
+ # actually has — mirroring how "Produce Content" (content-os) grants nothing and
426
+ # leans on the workspace's tools, rather than hardcoding a tool that might be absent.
427
+ playbooks:
428
+ - name: Enrich this lead
429
+ description: Take a thin, freshly-captured lead and fill it out — company, title, LinkedIn, and the relationships around it.
430
+ goalTemplate: >-
431
+ Enrich this lead ({lead}). Starting from whatever is known, fill out the
432
+ missing detail — company, job title, LinkedIn URL, location, and the people
433
+ and companies around them — and record it on the lead. Draw on whatever
434
+ enrichment capability this workspace has connected (e.g. an Apify or
435
+ Unipile/LinkedIn tool); if none is available, enrich from the public web and
436
+ what is already in the pod. Link the lead to its company and any related
437
+ contacts, and note where information is still missing.
438
+ subjectProfile:
439
+ profileSlug: lead
440
+ params:
441
+ - { name: lead, type: entity, label: Lead, required: true }
442
+ executor: is-agent
443
+ status: active
444
+
445
+ - name: Qualify this lead
446
+ description: Assess a lead against the ICP, score the fit, and decide the next step — advance, nurture, or disqualify.
447
+ goalTemplate: >-
448
+ Qualify this lead ({lead}). Assess how well they fit our ideal customer
449
+ profile — company, role, industry, size, and the signals of intent or need —
450
+ and set an ICP score. Decide the next step: advance them toward a deal,
451
+ keep nurturing, or disqualify. Write the reasoning and the decision back onto
452
+ the lead (its stage, score, and next step) so the call is auditable.
453
+ subjectProfile:
454
+ profileSlug: lead
455
+ params:
456
+ - { name: lead, type: entity, label: Lead, required: true }
457
+ executor: is-agent
458
+ status: active
459
+
460
+ - name: Work this deal
461
+ description: Move a deal toward Won — read the current stage, find the blocker, and propose the next step and owner.
462
+ goalTemplate: >-
463
+ Work this deal ({deal}) toward Won. Assess where it stands — its current
464
+ stage, value, and close date — and identify the single thing blocking it from
465
+ advancing. Propose the concrete next step, who owns it, and by when, and
466
+ update the deal's next step accordingly. If the stage should change, say so
467
+ and why.
468
+ subjectProfile:
469
+ profileSlug: deal
470
+ params:
471
+ - { name: deal, type: entity, label: Deal, required: true }
472
+ executor: is-agent
473
+ status: active
package/src/define.ts CHANGED
@@ -135,6 +135,18 @@ export interface WorkspaceDefinitionInput {
135
135
  /** VIEW half of the authored bento union — resolved against the viewMap. */
136
136
  bentoViewBlocks?: BentoViewBlockWire[];
137
137
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
138
+ /**
139
+ * Capability integrations (→ tools + skills + vault + capability-embedded
140
+ * automations/playbooks). Emitted from the template's `integrations:` list —
141
+ * IDENTICAL to what `toPackageDefinition` puts in its `capabilities` field. The
142
+ * tRPC door (`workspaces.createFromDefinition`) now declares `capabilities` in
143
+ * its zod input and installs them via the SHARED `applyPackagePostWorkspace`
144
+ * door (same as Hub `/packages/apply`). It was missing from this interface, so
145
+ * `toWorkspaceDefinition` had nowhere to put integrations and the browser
146
+ * BUNDLED install path carried NO capabilities — every capability's
147
+ * tools/skills/automations silently dropped on that door.
148
+ */
149
+ capabilities?: WorkspaceYaml['integrations'];
138
150
  // Workspace-level metadata (flattened from the `workspace` block)
139
151
  workspaceSubtype?: string;
140
152
  workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
@@ -203,6 +215,13 @@ export function toWorkspaceDefinition(
203
215
  bentoLayout: bento.bentoLayout,
204
216
  bentoViewBlocks: bento.bentoViewBlocks,
205
217
  profileEntityBentoTemplates: tpl.profileEntityBentoTemplates,
218
+ // Capability integrations — forwarded so the tRPC/UI (bundled) install door
219
+ // seeds the SAME operational payload (tools + skills + capability-embedded
220
+ // automations/playbooks) the CLI/Hub door gets via `toPackageDefinition`.
221
+ // `workspaces.createFromDefinition` declares `capabilities` in its zod input
222
+ // and routes it through the shared `applyPackagePostWorkspace` door. Omitting
223
+ // it here made the browser bundled-install path carry no capabilities at all.
224
+ capabilities: tpl.integrations,
206
225
  workspaceSubtype: ws.subtype,
207
226
  workspaceType: ws.workspaceType,
208
227
  workspaceVisibility: ws.visibility,
@@ -261,6 +280,7 @@ type WorkspaceDefinitionForwarded =
261
280
  | 'layout' // → layoutConfig
262
281
  | 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
263
282
  | 'profileEntityBentoTemplates'
283
+ | 'integrations' // → capabilities (installed via shared applyPackagePostWorkspace door)
264
284
  | 'onboarding'
265
285
  | 'dependencies';
266
286
 
@@ -277,16 +297,6 @@ type WorkspaceDefinitionDropped =
277
297
  // The door DOES accept `extends`, but no template declares it today (0/21).
278
298
  // Forwarding is a no-op until one does — wire it then.
279
299
  | 'extends'
280
- // The door DOES accept `automations`, but no template declares it today
281
- // (0/21), and its zod shape ({trigger,action:{playbook_run}}) is not
282
- // `TemplateAutomation`. Needs a mapper if a template ever uses it.
283
- // The browser install path installs automations via its own createAutomation door.
284
- | 'automations'
285
- // The door has no `integrations` field. Its `tools` field is a DIFFERENT
286
- // shape ({name,kind,inputSchema} vs {templateKey,params,mode}). Capability
287
- // templates are seeded via the packages/apply path, not createFromDefinition
288
- // — see the note on `TemplateIntegration` in types.ts. Forwarding = no-op.
289
- | 'integrations'
290
300
  // REAL GAP (agent-fleet, project-management declare playbooks). The door
291
301
  // accepts `playbooks`, but the shapes disagree: template param `type` allows
292
302
  // 'text'|'entity'|'choice', the tRPC enum accepts only string|number|boolean
@@ -446,12 +456,10 @@ type PackageDefinitionDropped =
446
456
  // UI-only, same as the definition door: `PackageDefinition` has no `app`.
447
457
  | 'app'
448
458
  // REAL GAP: backend `PackageDefinition` DOES declare `extends`
449
- // (package-definition.ts:262) and `automations` (:268), but
450
- // `PackageDefinitionOutput` omits both. No template declares either today
451
- // (0/21), so this is latent, not live — wire them when a template first uses
452
- // one.
453
- | 'extends'
454
- | 'automations';
459
+ // (package-definition.ts:262), but `PackageDefinitionOutput` omits it. No
460
+ // template declares it today (0/21), so this is latent, not live — wire it
461
+ // when a template first uses one.
462
+ | 'extends';
455
463
 
456
464
  type UnclassifiedPackageDefinitionKey = Exclude<
457
465
  keyof WorkspaceYaml,
@@ -368,6 +368,70 @@ entityLinks:
368
368
  type: involves
369
369
  label: Involves
370
370
 
371
+ # Playbooks — session templates seeded with the workspace.
372
+ # Ported from the orphaned `deep-research.playbook.json` (CP templates/packs):
373
+ # same goalTemplate + companyContext param + is-agent executor + Web-Search /
374
+ # Entity-Creator grants + typed outputs (encoded via the {outputTypes} param).
375
+ # `subjectProfile` binds it to the shared `competitor` role so the Capture →
376
+ # Session matcher can offer it when a competitor is captured; the captured
377
+ # competitor is passed as the `competitor` entity param.
378
+ playbooks:
379
+ - name: Research Competitor
380
+ description: Deep-research a competitor and position it against us — strengths, weaknesses, pricing, and the gaps we can exploit.
381
+ goalTemplate: >-
382
+ Research this competitor ({competitor}) and compare them to us. Investigate
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.
387
+ subjectProfile:
388
+ profileSlug: competitor
389
+ params:
390
+ - { name: competitor, type: entity, label: Competitor, required: true }
391
+ - { name: focus, type: text, label: Focus area, defaultValue: "product, pricing, and positioning" }
392
+ - { name: companyContext, type: text, label: Our company context }
393
+ - { name: outputTypes, type: text, label: Outputs, defaultValue: "a head-to-head comparison summary and an updated competitor profile" }
394
+ executor: is-agent
395
+ # A radar MONITOR: bound to the competitor KIND + a schedule → the backing
396
+ # cron automation fans out (query→loop→playbook_run) and runs this once per
397
+ # competitor. Disabled by default so installing the workspace never silently
398
+ # starts paid AI runs — the user turns it on when they want the watch.
399
+ schedule: { cron: "0 9 * * 1", enabled: false }
400
+ grants:
401
+ - Web Search
402
+ - Entity Creator
403
+ status: active
404
+
405
+ # A radar DISCOVERY run: the self-generating half. No subjectProfile → a single
406
+ # scheduled run (not per-existing-entity) that finds competitors we don't track
407
+ # yet and adds them — deduplicated by website — so the knowledge base grows
408
+ # itself. Requires the Exa capability (integrations below).
409
+ - name: Discover Competitors
410
+ 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
+ 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.
420
+ schedule: { cron: "0 9 * * 1", enabled: false }
421
+ params:
422
+ - { name: ourSpace, type: text, label: Our market / category, required: true }
423
+ executor: is-agent
424
+ grants:
425
+ - exa_search
426
+ - Entity Creator
427
+ status: active
428
+
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.
432
+ integrations:
433
+ - templateKey: exa
434
+
371
435
  onboarding:
372
436
  goal: >
373
437
  Map the real ecosystem — the key players (companies and people), who partners
@@ -10,6 +10,7 @@
10
10
  # • ecosystem — the market graph (partners, competitors, segments, trends)
11
11
  # • operations — how the company runs (the operational lens)
12
12
  # • crm — the pipeline (contacts, leads, clients, deals)
13
+ # • content-os — the editorial engine (pillars, posts, platforms, newsletters)
13
14
  #
14
15
  # It uses `require` (not `compose`): each constituent is a REAL workspace, not an
15
16
  # overlay. Deps-first resolution installs foundation first (the SSOT role
@@ -46,6 +47,10 @@ dependencies:
46
47
  kind: workspace
47
48
  relation: require
48
49
  reason: "The pipeline — contacts, leads, clients, and deals."
50
+ - slug: content-os
51
+ kind: workspace
52
+ relation: require
53
+ reason: "The editorial engine — pillars, posts, platforms, and newsletters — so the company's content operation is part of the operating core."
49
54
 
50
55
  workspace:
51
56
  name: Enterprise OS
package/src/index.ts CHANGED
@@ -34,7 +34,6 @@ export type {
34
34
  TemplateSuggestedRelation,
35
35
  TemplateDisplayTemplate,
36
36
  TemplateIntegration,
37
- TemplateAutomation,
38
37
  TemplateExtendsRef,
39
38
  TemplateDependency,
40
39
  PackageDependencyKind,
@@ -62,7 +61,7 @@ export {
62
61
 
63
62
  export type { WorkspaceTemplateSlug } from "./templates.js";
64
63
 
65
- export { mergeCatalog } from "./catalog.js";
64
+ export { mergeCatalog, rowToRemoteEntry, assembleCatalog } from "./catalog.js";
66
65
  export type {
67
66
  MergeCatalogInput,
68
67
  MergeCatalogResult,
@@ -72,6 +71,8 @@ export type {
72
71
  CatalogEntry,
73
72
  BundledCatalogEntry,
74
73
  RemoteCatalogEntry,
74
+ CpPackageRow,
75
+ AssembleCatalogInput,
75
76
  } from "./catalog.js";
76
77
 
77
78
  export {