@synap-core/workspace-templates 0.23.0 → 0.24.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 (42) hide show
  1. package/README.md +50 -24
  2. package/capabilities/stripe.capability.json +1163 -87
  3. package/capabilities/unipile-linkedin.capability.json +30 -7
  4. package/dist/{define-CPS0n8BV.d.ts → define-DqoItRiR.d.ts} +40 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +159 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/wire.d.ts +1 -1
  9. package/package.json +3 -3
  10. package/skills/app-factory-design-review/SKILL.md +8 -2
  11. package/skills/design-critique/SKILL.md +3 -0
  12. package/src/catalog-safety.test.ts +1 -1
  13. package/src/content-os.template.test.ts +27 -0
  14. package/src/content-os.yaml +43 -0
  15. package/src/crm-messages-cadence.template.test.ts +83 -1
  16. package/src/crm.yaml +111 -4
  17. package/src/define.ts +16 -0
  18. package/src/reserved-profile-slugs.test.ts +8 -10
  19. package/src/retired-goal-kinds.test.ts +6 -12
  20. package/src/stripe-capability.test.ts +192 -0
  21. package/src/template-documents.test.ts +158 -0
  22. package/src/templates.ts +92 -4
  23. package/src/types.ts +32 -1
  24. package/src/unipile-linkedin-find-profile.test.ts +183 -0
  25. package/src/validate.ts +81 -0
  26. package/views/bookmark-views-pack.view.json +39 -0
  27. package/views/event-views-pack.view.json +45 -0
  28. package/views/task-views-pack.view.json +53 -0
  29. package/packs/bookmark-views-pack.json +0 -33
  30. package/packs/crm-pack.json +0 -76
  31. package/packs/customer-profile.json +0 -28
  32. package/packs/deep-research.playbook.json +0 -30
  33. package/packs/event-views-pack.json +0 -33
  34. package/packs/habit-profile.json +0 -27
  35. package/packs/hr-pack.json +0 -54
  36. package/packs/invoice-profile.json +0 -28
  37. package/packs/knowledge-base-pack.json +0 -56
  38. package/packs/meeting-profile.json +0 -29
  39. package/packs/product-profile.json +0 -30
  40. package/packs/project-management-pack.json +0 -49
  41. package/packs/recipe-profile.json +0 -29
  42. package/packs/task-views-pack.json +0 -39
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Unipile — finding ONE known person's LinkedIn profile, and paging the people
3
+ * search (CRM wave 2, S4b, build map 6f6c197f).
4
+ *
5
+ * - `linkedin_find_profile` answers a profile only on a STRICT match: the
6
+ * result's name equals the full name (case- and accent-insensitive), the
7
+ * company is in its headline as whole words, and exactly one result is
8
+ * both. Otherwise no match, with up to 3 candidates for a person to choose
9
+ * from. It writes nothing: the match's fields are the caller's proposal
10
+ * (founder decision 7).
11
+ * - `linkedin_search_people` and `linkedin_capture_profiles` page by
12
+ * `cursor`, the reply's `next_cursor` (Unipile v2 performClassicPeopleSearch
13
+ * takes a `cursor` query param and no offset or limit).
14
+ *
15
+ * SEAM: each verb's REAL `code` body, run the way the pod's executor wraps it
16
+ * (`async (args, context) => { <code> }` with `callProvider` and `propose` in
17
+ * scope), against a recorded `callProvider` that answers page by page.
18
+ *
19
+ * NOT covered: what a live LinkedIn search returns (ranking, how the
20
+ * `advanced_keywords.company` filter matches, headline wording), and the pod's
21
+ * governance of the read (finding 4c31855b: a read once came back proposed;
22
+ * the verb then returns `proposed`, tested below with a stubbed answer).
23
+ */
24
+ import { readFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { describe, expect, it } from "vitest";
27
+
28
+ const capability = JSON.parse(
29
+ readFileSync(join(import.meta.dirname, "..", "capabilities", "unipile-linkedin.capability.json"), "utf8"),
30
+ ) as { spec: { skills: Array<Record<string, any>> } };
31
+ const skill = (name: string) => capability.spec.skills.find((s) => s.name === name)!;
32
+
33
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
34
+ ...args: string[]
35
+ ) => (...a: unknown[]) => Promise<any>;
36
+
37
+ type Call = { tool: string; method: string; path: string; body: Record<string, unknown> };
38
+ type Page = { data: Array<Record<string, unknown>>; next_cursor?: string | null };
39
+
40
+ /** Runs `name`'s code; `pages` answers the provider calls in order (the last one repeats). */
41
+ async function run(name: string, args: Record<string, unknown>, pages: Array<Page | Record<string, unknown>>) {
42
+ const calls: Call[] = [];
43
+ const proposals: Array<Record<string, unknown>> = [];
44
+ const callProvider = async (tool: string, method: string, path: string, body: Record<string, unknown>) => {
45
+ calls.push({ tool, method, path, body });
46
+ const reply = pages[Math.min(calls.length - 1, pages.length - 1)];
47
+ return "proposed" in reply ? reply : { status: 200, body: reply };
48
+ };
49
+ const propose = { entity: async (input: Record<string, unknown>) => (proposals.push(input), { status: "proposed" }) };
50
+ const body = new AsyncFunction("args", "context", "callProvider", "propose", skill(name).code);
51
+ const result = await body(args, {}, callProvider, propose);
52
+ return { result, calls, proposals };
53
+ }
54
+
55
+ const row = (id: string, display_name: string, headline: string) => ({
56
+ object: "PeopleSearchResult",
57
+ id,
58
+ display_name,
59
+ headline,
60
+ public_identifier: `${id}-slug`,
61
+ profile_url: `https://www.linkedin.com/in/${id}-slug`,
62
+ network_distance: "SECOND_DEGREE",
63
+ });
64
+ const ADA = row("u-ada", "Adèle Lovelace", "CEO at Analytical Engines · Paris");
65
+ const ARGS = { accountId: "acc_1", fullName: "adele LOVELACE", company: "Analytical Engines" };
66
+
67
+ describe("linkedin_find_profile — a strict match or none", () => {
68
+ const verb = skill("linkedin_find_profile");
69
+
70
+ it("is a read the pod runs at once, routed by enrich_entity, and carries no install-time value", () => {
71
+ expect(verb.kind).toBe("code");
72
+ expect(verb.intent).toBe("enrich_entity");
73
+ expect(verb.metadata).toEqual({ verbType: "read" });
74
+ expect(verb.parameters).toEqual({ fullName: "string", company: "string", accountId: "string" });
75
+ expect(verb.requires).toEqual(["unipile_linkedin"]);
76
+ expect(verb.code).not.toMatch(/\{\{\w+\}\}/);
77
+ // It writes nothing: the match is the caller's proposal (decision 7).
78
+ expect(verb.code).not.toMatch(/propose\.entity|callProvider\([^)]*'(PATCH|PUT|DELETE)'/);
79
+ });
80
+
81
+ it("one result with the same name (case and accents aside) and the company in its headline: the match", async () => {
82
+ const other = row("u-2", "Ada Byron", "Engineer at Analytical Engines");
83
+ const { result, calls, proposals } = await run("linkedin_find_profile", ARGS, [{ data: [other, ADA], next_cursor: null }]);
84
+ expect(result.matched).toBe(true);
85
+ expect(result.profile).toMatchObject({ name: "Adèle Lovelace", providerId: "u-ada", publicIdentifier: "u-ada-slug" });
86
+ expect(result.properties).toEqual({ linkedinUrl: "https://www.linkedin.com/in/u-ada-slug", linkedinProviderId: "u-ada" });
87
+ expect(result.candidates).toEqual([]);
88
+ expect(proposals).toEqual([]);
89
+ expect(calls).toEqual([
90
+ {
91
+ tool: "unipile_linkedin",
92
+ method: "POST",
93
+ path: "/acc_1/linkedin/search/people",
94
+ body: { keywords: "adele LOVELACE", advanced_keywords: { company: "Analytical Engines" } },
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it("two results that both match: no match, both offered as candidates", async () => {
100
+ const twin = row("u-twin", "Adele Lovelace", "Analytical Engines, board member");
101
+ const { result } = await run("linkedin_find_profile", ARGS, [{ data: [ADA, twin] }]);
102
+ expect(result).toMatchObject({ matched: false, reason: "several" });
103
+ expect(result.candidates.map((c: any) => c.providerId)).toEqual(["u-ada", "u-twin"]);
104
+ expect(result.properties).toBeUndefined();
105
+ });
106
+
107
+ it("the name matches but the company is not in the headline, or only inside another word: no match", async () => {
108
+ const elsewhere = row("u-3", "Adèle Lovelace", "Founder at Difference Engines");
109
+ const inside = row("u-4", "Adèle Lovelace", "CTO at AnalyticalEnginesCo");
110
+ const { result } = await run("linkedin_find_profile", ARGS, [{ data: [elsewhere, inside] }]);
111
+ expect(result).toMatchObject({ matched: false, reason: "none" });
112
+ expect(result.candidates.map((c: any) => [c.providerId, c.nameMatches, c.companyInHeadline])).toEqual([
113
+ ["u-3", true, false],
114
+ ["u-4", true, false],
115
+ ]);
116
+ });
117
+
118
+ it("a different name with the right company is never a match, and at most 3 candidates come back, closest first", async () => {
119
+ const rows = [
120
+ row("a", "Ada Lovelace", "CEO at Analytical Engines"),
121
+ row("b", "Bob Smith", "Sales"),
122
+ row("c", "Adèle Lovelace", "Poet"),
123
+ row("d", "Carol King", "Analytical Engines"),
124
+ ];
125
+ const { result } = await run("linkedin_find_profile", ARGS, [{ data: rows }]);
126
+ expect(result).toMatchObject({ matched: false, reason: "none" });
127
+ expect(result.candidates.map((c: any) => c.providerId)).toEqual(["c", "a", "d"]);
128
+ });
129
+
130
+ it("pages by cursor: a match on page 2 with no page after it is the match", async () => {
131
+ const { result, calls } = await run("linkedin_find_profile", ARGS, [
132
+ { data: [row("x", "Someone Else", "Analytical Engines")], next_cursor: "c2" },
133
+ { data: [ADA], next_cursor: null },
134
+ ]);
135
+ expect(result.matched).toBe(true);
136
+ expect(calls.map((c) => c.path)).toEqual(["/acc_1/linkedin/search/people", "/acc_1/linkedin/search/people?cursor=c2"]);
137
+ });
138
+
139
+ it("a third page left unread could hold another match: no match (more_results), and it stops after 2 pages", async () => {
140
+ const { result, calls } = await run("linkedin_find_profile", ARGS, [
141
+ { data: [ADA], next_cursor: "c2" },
142
+ { data: [row("y", "Other", "x")], next_cursor: "c3" },
143
+ ]);
144
+ expect(result).toMatchObject({ matched: false, reason: "more_results" });
145
+ expect(result.searched).toMatchObject({ pages: 2, moreUnread: true });
146
+ expect(calls).toHaveLength(2);
147
+ });
148
+
149
+ it("a search the pod held for review is no match and says so", async () => {
150
+ const { result } = await run("linkedin_find_profile", ARGS, [{ proposed: true, proposalId: "p-1" }]);
151
+ expect(result).toEqual({ matched: false, reason: "proposed", proposed: true, proposalId: "p-1", candidates: [] });
152
+ });
153
+
154
+ it("a failed search throws; a missing company or name is refused before any call", async () => {
155
+ await expect(run("linkedin_find_profile", ARGS, [{ proposed: false, status: 429 } as any])).rejects.toThrow(/status 429/);
156
+ await expect(run("linkedin_find_profile", { ...ARGS, company: " " }, [{ data: [] }])).rejects.toThrow(/company is required/);
157
+ await expect(run("linkedin_find_profile", { ...ARGS, fullName: "" }, [{ data: [] }])).rejects.toThrow(/fullName/);
158
+ });
159
+ });
160
+
161
+ describe("the people search pages by cursor", () => {
162
+ it.each(["linkedin_search_people", "linkedin_capture_profiles"])("%s takes a cursor and returns the next one", async (name) => {
163
+ expect(skill(name).parameters.cursor).toBe("string?");
164
+ const first = await run(name, { accountId: "acc_1", keywords: "founders lyon" }, [{ data: [ADA], next_cursor: "n2" }]);
165
+ expect(first.calls[0].path).toBe("/acc_1/linkedin/search/people");
166
+ expect(first.calls[0].body).toEqual({ keywords: "founders lyon" });
167
+ expect(first.result.cursor).toBe("n2");
168
+
169
+ const second = await run(name, { accountId: "acc_1", keywords: "founders lyon", cursor: "n2" }, [{ data: [ADA], next_cursor: null }]);
170
+ expect(second.calls[0].path).toBe("/acc_1/linkedin/search/people?cursor=n2");
171
+ expect(second.result.cursor).toBeNull();
172
+ // v2 takes no offset or limit: page 2 is reached by the cursor alone (the
173
+ // exact paths above carry no other query param).
174
+ });
175
+
176
+ it("capture still proposes each row of the page, and search still returns them", async () => {
177
+ const capture = await run("linkedin_capture_profiles", { accountId: "acc_1", keywords: "k", cursor: "n2" }, [{ data: [ADA] }]);
178
+ expect(capture.proposals).toHaveLength(1);
179
+ expect(capture.proposals[0]).toMatchObject({ profileSlug: "person", title: "Adèle Lovelace" });
180
+ const search = await run("linkedin_search_people", { accountId: "acc_1", keywords: "k", limit: 1 }, [{ data: [ADA, ADA] }]);
181
+ expect(search.result.count).toBe(1);
182
+ });
183
+ });
package/src/validate.ts CHANGED
@@ -282,6 +282,31 @@ export function collectBaseProfileSlugs(
282
282
  return out;
283
283
  }
284
284
 
285
+ /** A template document slug: kebab-case. Same rule as the pod's install door (`schemas/template-document.ts`). */
286
+ export const TEMPLATE_DOCUMENT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
287
+
288
+ /**
289
+ * The document slugs of every base this template COMPOSES onto, transitively —
290
+ * an overlay installs into its base's space, so its sidebar may name them. A
291
+ * `require` base keeps its own space: its documents are not reachable.
292
+ */
293
+ function composeBaseDocumentSlugs(
294
+ data: WorkspaceYaml,
295
+ resolveDep: (slug: string) => WorkspaceYaml | undefined,
296
+ seen = new Set<string>()
297
+ ): Set<string> {
298
+ const out = new Set<string>();
299
+ for (const dep of data.dependencies || []) {
300
+ if (dep.relation !== "compose" || seen.has(dep.slug)) continue;
301
+ seen.add(dep.slug);
302
+ const base = resolveDep(dep.slug);
303
+ if (!base) continue;
304
+ for (const d of base.documents || []) if (d?.slug) out.add(d.slug);
305
+ for (const s of composeBaseDocumentSlugs(base, resolveDep, seen)) out.add(s);
306
+ }
307
+ return out;
308
+ }
309
+
285
310
  /**
286
311
  * Recursively collect every `profileSlug` string value under `node`, tagged with
287
312
  * a dotted path for error messages. Used for `automations` (whose flow-node /
@@ -2014,6 +2039,62 @@ export function validateTemplate(
2014
2039
  );
2015
2040
  }
2016
2041
  }
2042
+
2043
+ // ── Template documents (`documents[]`) ─────────────────────────────────────
2044
+ // The pod knows a template document by its slug in the space (it never
2045
+ // rewrites or recreates one), so a slug must be stable, unique and named by
2046
+ // every document sidebar item. A compose overlay installs into its base's
2047
+ // space, so it may also name a document its base carries.
2048
+ const documentSlugs = new Set<string>();
2049
+ for (const [i, d] of (tpl.documents || []).entries()) {
2050
+ const where = `documents[${i}]`;
2051
+ if (typeof d?.slug !== "string" || !TEMPLATE_DOCUMENT_SLUG_RE.test(d.slug)) {
2052
+ add(
2053
+ "document-slug-invalid",
2054
+ `${where}.slug`,
2055
+ `${where}.slug "${d?.slug ?? ""}" must be kebab-case (a-z, 0-9, -): the space knows the document by it`
2056
+ );
2057
+ continue;
2058
+ }
2059
+ if (documentSlugs.has(d.slug))
2060
+ add(
2061
+ "document-slug-duplicate",
2062
+ `${where}.slug`,
2063
+ `document slug "${d.slug}" is declared twice`
2064
+ );
2065
+ documentSlugs.add(d.slug);
2066
+ if (typeof d.title !== "string" || !d.title.trim())
2067
+ add(
2068
+ "document-title-required",
2069
+ `${where}.title`,
2070
+ `document "${d.slug}" needs a title (its name)`
2071
+ );
2072
+ if (typeof d.content !== "string")
2073
+ add(
2074
+ "document-content-required",
2075
+ `${where}.content`,
2076
+ `document "${d.slug}" needs a markdown content (it may be empty)`
2077
+ );
2078
+ if (d.scope !== undefined && d.scope !== "space" && d.scope !== "pod")
2079
+ add(
2080
+ "document-scope-unknown",
2081
+ `${where}.scope`,
2082
+ `document "${d.slug}" scope "${String(d.scope)}" is not one of: space, pod`
2083
+ );
2084
+ }
2085
+ const reachableDocumentSlugs = new Set([
2086
+ ...documentSlugs,
2087
+ ...composeBaseDocumentSlugs(tpl, resolveDepFrom(opts)),
2088
+ ]);
2089
+ for (const [i, si] of (tpl.layout?.sidebarItems || []).entries()) {
2090
+ if (si.kind !== "document") continue;
2091
+ if (!si.documentSlug || !reachableDocumentSlugs.has(si.documentSlug))
2092
+ add(
2093
+ "sidebar-document-unknown",
2094
+ `layout.sidebarItems[${i}]`,
2095
+ `layout.sidebarItems ("${si.label ?? si.documentSlug ?? "document"}") names document "${si.documentSlug ?? ""}", which this template (or a base it composes onto) does not declare in documents[]`
2096
+ );
2097
+ }
2017
2098
  for (const dt of tpl.displayTemplates || []) {
2018
2099
  if (dt.entityType && !known(dt.entityType)) {
2019
2100
  add(
@@ -0,0 +1,39 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "view",
4
+ "meta": {
5
+ "slug": "bookmark-views-pack",
6
+ "name": "Bookmark Views",
7
+ "description": "Three ready-made views for saved links and articles: a masonry reading feed, a gallery and an articles table.",
8
+ "icon": "bookmark",
9
+ "color": "#F59E0B",
10
+ "tags": ["bookmarks", "reading", "web", "views"],
11
+ "isPublic": true
12
+ },
13
+ "spec": {
14
+ "views": [
15
+ {
16
+ "slug": "reading-feed",
17
+ "displayName": "Reading Feed",
18
+ "type": "masonry",
19
+ "config": {},
20
+ "scopeProfileSlugs": ["bookmark", "website", "article"]
21
+ },
22
+ {
23
+ "slug": "bookmark-gallery",
24
+ "displayName": "Bookmark Gallery",
25
+ "type": "gallery",
26
+ "config": {},
27
+ "scopeProfileSlugs": ["bookmark", "website", "article"]
28
+ },
29
+ {
30
+ "slug": "articles-table",
31
+ "displayName": "Articles Table",
32
+ "type": "table",
33
+ "config": {},
34
+ "scopeProfileSlugs": ["article"]
35
+ }
36
+ ]
37
+ },
38
+ "dependencies": []
39
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "view",
4
+ "meta": {
5
+ "slug": "event-views-pack",
6
+ "name": "Event Views",
7
+ "description": "Three ready-made event views: a calendar, an upcoming-events list and an events table.",
8
+ "icon": "calendar",
9
+ "color": "#10B981",
10
+ "tags": ["events", "calendar", "schedule", "views"],
11
+ "isPublic": true
12
+ },
13
+ "spec": {
14
+ "views": [
15
+ {
16
+ "slug": "event-calendar",
17
+ "displayName": "Event Calendar",
18
+ "type": "calendar",
19
+ "config": {
20
+ "dateField": "startDate",
21
+ "endDateField": "endDate"
22
+ },
23
+ "scopeProfileSlugs": ["event"]
24
+ },
25
+ {
26
+ "slug": "upcoming-events",
27
+ "displayName": "Upcoming Events",
28
+ "type": "list",
29
+ "config": {
30
+ "sortBy": "startDate",
31
+ "sortOrder": "asc"
32
+ },
33
+ "scopeProfileSlugs": ["event"]
34
+ },
35
+ {
36
+ "slug": "events-table",
37
+ "displayName": "Events Table",
38
+ "type": "table",
39
+ "config": {},
40
+ "scopeProfileSlugs": ["event"]
41
+ }
42
+ ]
43
+ },
44
+ "dependencies": []
45
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "view",
4
+ "meta": {
5
+ "slug": "task-views-pack",
6
+ "name": "Task Views",
7
+ "description": "Four ready-made task views: a Kanban board by status, a priority matrix, a due-date calendar and a table of all tasks.",
8
+ "icon": "layout-grid",
9
+ "color": "#3B82F6",
10
+ "tags": ["tasks", "kanban", "views", "productivity"],
11
+ "isPublic": true
12
+ },
13
+ "spec": {
14
+ "views": [
15
+ {
16
+ "slug": "task-board",
17
+ "displayName": "Task Board",
18
+ "type": "kanban",
19
+ "config": {
20
+ "groupByField": "status"
21
+ },
22
+ "scopeProfileSlugs": ["task"]
23
+ },
24
+ {
25
+ "slug": "priority-matrix",
26
+ "displayName": "Priority Matrix",
27
+ "type": "matrix",
28
+ "config": {
29
+ "xField": "priority",
30
+ "yField": "status"
31
+ },
32
+ "scopeProfileSlugs": ["task"]
33
+ },
34
+ {
35
+ "slug": "task-calendar",
36
+ "displayName": "Task Calendar",
37
+ "type": "calendar",
38
+ "config": {
39
+ "dateField": "dueDate"
40
+ },
41
+ "scopeProfileSlugs": ["task"]
42
+ },
43
+ {
44
+ "slug": "all-tasks",
45
+ "displayName": "All Tasks",
46
+ "type": "table",
47
+ "config": {},
48
+ "scopeProfileSlugs": ["task"]
49
+ }
50
+ ]
51
+ },
52
+ "dependencies": []
53
+ }
@@ -1,33 +0,0 @@
1
- {
2
- "_meta": {
3
- "slug": "bookmark-views-pack",
4
- "displayName": "Bookmark Views",
5
- "icon": "bookmark",
6
- "color": "#F59E0B",
7
- "tags": ["bookmarks", "reading", "web", "views"],
8
- "requiredTier": null,
9
- "isPublic": true
10
- },
11
- "description": "Three ready-made views for saved links and articles: a masonry reading feed, a gallery and an articles table.",
12
- "profiles": [],
13
- "views": [
14
- {
15
- "name": "Reading Feed",
16
- "type": "masonry",
17
- "scopeProfileSlugs": ["bookmark", "website", "article"],
18
- "config": {}
19
- },
20
- {
21
- "name": "Bookmark Gallery",
22
- "type": "gallery",
23
- "scopeProfileSlugs": ["bookmark", "website", "article"],
24
- "config": {}
25
- },
26
- {
27
- "name": "Articles Table",
28
- "type": "table",
29
- "scopeProfileSlug": "article",
30
- "config": {}
31
- }
32
- ]
33
- }
@@ -1,76 +0,0 @@
1
- {
2
- "_meta": {
3
- "slug": "crm-pack",
4
- "icon": "users",
5
- "color": "#3B82F6",
6
- "tags": ["crm", "sales", "contacts", "deals"],
7
- "requiredTier": null,
8
- "isPublic": true
9
- },
10
- "profiles": [
11
- {
12
- "slug": "contact",
13
- "displayName": "Contact",
14
- "icon": "user-check",
15
- "color": "#06B6D4",
16
- "description": "People you interact with",
17
- "scope": "workspace",
18
- "parentProfileSlug": "person",
19
- "properties": [
20
- { "slug": "role", "valueType": "string", "label": "Role / Title" },
21
- { "slug": "department", "valueType": "string", "label": "Department" },
22
- { "slug": "source", "valueType": "string", "label": "Lead Source", "inputType": "select", "enumValues": ["referral", "website", "event", "cold-outreach", "other"] },
23
- { "slug": "last-contacted", "valueType": "date", "label": "Last Contacted" }
24
- ]
25
- },
26
- {
27
- "slug": "company",
28
- "displayName": "Company",
29
- "icon": "building",
30
- "color": "#6366F1",
31
- "description": "Organizations and businesses",
32
- "scope": "workspace",
33
- "properties": [
34
- { "slug": "website", "valueType": "string", "label": "Website", "inputType": "url" },
35
- { "slug": "industry", "valueType": "string", "label": "Industry" },
36
- { "slug": "employees", "valueType": "number", "label": "Employee Count", "constraints": { "min": 0 } },
37
- { "slug": "location", "valueType": "string", "label": "Location" },
38
- { "slug": "revenue", "valueType": "number", "label": "Annual Revenue", "constraints": { "min": 0 } }
39
- ]
40
- },
41
- {
42
- "slug": "deal",
43
- "displayName": "Deal",
44
- "icon": "trending-up",
45
- "color": "#EC4899",
46
- "description": "Sales opportunities and pipeline",
47
- "scope": "workspace",
48
- "properties": [
49
- { "slug": "value", "valueType": "number", "required": true, "label": "Deal Value", "constraints": { "min": 0 } },
50
- { "slug": "stage", "valueType": "string", "required": true, "label": "Stage", "inputType": "select", "enumValues": ["lead", "qualified", "proposal", "negotiation", "closed-won", "closed-lost"] },
51
- { "slug": "close-date", "valueType": "date", "label": "Expected Close" },
52
- { "slug": "probability", "valueType": "number", "label": "Win Probability (%)", "constraints": { "min": 0, "max": 100 } },
53
- { "slug": "notes", "valueType": "string", "label": "Notes", "inputType": "textarea" }
54
- ]
55
- }
56
- ],
57
- "views": [
58
- {
59
- "name": "Pipeline",
60
- "type": "kanban",
61
- "scopeProfileSlug": "deal",
62
- "config": { "groupByField": "stage" }
63
- },
64
- {
65
- "name": "Contacts",
66
- "type": "table",
67
- "scopeProfileSlug": "contact",
68
- "config": {}
69
- }
70
- ],
71
- "entityLinks": [
72
- { "sourceProfileSlug": "contact", "targetProfileSlug": "company", "type": "works_at", "label": "Works at" },
73
- { "sourceProfileSlug": "deal", "targetProfileSlug": "contact", "type": "owned_by", "label": "Owned by" },
74
- { "sourceProfileSlug": "deal", "targetProfileSlug": "company", "type": "deal_for", "label": "Deal for" }
75
- ]
76
- }
@@ -1,28 +0,0 @@
1
- {
2
- "_meta": {
3
- "slug": "customer-profile",
4
- "icon": "user-check",
5
- "color": "#3B82F6",
6
- "tags": ["customer", "crm", "contact"],
7
- "requiredTier": null,
8
- "isPublic": true
9
- },
10
- "profiles": [
11
- {
12
- "slug": "customer",
13
- "displayName": "Customer",
14
- "icon": "user-check",
15
- "color": "#3B82F6",
16
- "description": "Customer or client contact with deal tracking",
17
- "scope": "workspace",
18
- "properties": [
19
- { "slug": "name", "valueType": "string", "required": true, "label": "Full Name" },
20
- { "slug": "email", "valueType": "string", "label": "Email", "inputType": "email" },
21
- { "slug": "phone", "valueType": "string", "label": "Phone" },
22
- { "slug": "company", "valueType": "string", "label": "Company" },
23
- { "slug": "status", "valueType": "string", "label": "Status", "inputType": "select", "enumValues": ["lead", "prospect", "active", "churned"] },
24
- { "slug": "notes", "valueType": "string", "label": "Notes", "inputType": "textarea" }
25
- ]
26
- }
27
- ]
28
- }
@@ -1,30 +0,0 @@
1
- {
2
- "_meta": {
3
- "slug": "deep-research",
4
- "type": "playbook",
5
- "icon": "search",
6
- "color": "#10B981",
7
- "tags": ["research", "analysis", "automation"],
8
- "isPublic": true,
9
- "description": "Autonomous deep research playbook. Researches a domain, finds the best tools/platforms, and produces a CSV report and entity pool."
10
- },
11
- "name": "Deep Research",
12
- "goalTemplate": "Research {domain}. Find the best {category} and produce: {outputTypes}. Use the provided company context where relevant.",
13
- "description": "Researches a domain, identifies the best tools and platforms, produces structured outputs.",
14
- "params": [
15
- { "name": "domain", "type": "string", "default": "AI coding tools" },
16
- { "name": "category", "type": "string", "default": "platforms and frameworks" },
17
- { "name": "outputTypes", "type": "string", "default": "CSV file and entity pool" },
18
- { "name": "companyContext", "type": "string", "default": "" }
19
- ],
20
- "executor": "is-agent",
21
- "grants": [
22
- { "kind": "tool", "ref": "Web Search" },
23
- { "kind": "skill", "ref": "Entity Creator" }
24
- ],
25
- "expectedOutputs": [
26
- { "description": "Research findings as structured CSV", "type": "csv" },
27
- { "description": "Entity pool of discovered tools and companies", "type": "entities" },
28
- { "description": "Summary report with recommendations", "type": "report" }
29
- ]
30
- }
@@ -1,33 +0,0 @@
1
- {
2
- "_meta": {
3
- "slug": "event-views-pack",
4
- "displayName": "Event Views",
5
- "icon": "calendar",
6
- "color": "#10B981",
7
- "tags": ["events", "calendar", "schedule", "views"],
8
- "requiredTier": null,
9
- "isPublic": true
10
- },
11
- "description": "Three ready-made event views: a calendar, an upcoming-events list and an events table.",
12
- "profiles": [],
13
- "views": [
14
- {
15
- "name": "Event Calendar",
16
- "type": "calendar",
17
- "scopeProfileSlug": "event",
18
- "config": { "dateField": "startDate", "endDateField": "endDate" }
19
- },
20
- {
21
- "name": "Upcoming Events",
22
- "type": "list",
23
- "scopeProfileSlug": "event",
24
- "config": { "sortBy": "startDate", "sortOrder": "asc" }
25
- },
26
- {
27
- "name": "Events Table",
28
- "type": "table",
29
- "scopeProfileSlug": "event",
30
- "config": {}
31
- }
32
- ]
33
- }
@@ -1,27 +0,0 @@
1
- {
2
- "_meta": {
3
- "slug": "habit-profile",
4
- "icon": "repeat",
5
- "color": "#10B981",
6
- "tags": ["habit", "tracking", "wellness"],
7
- "requiredTier": null,
8
- "isPublic": true
9
- },
10
- "profiles": [
11
- {
12
- "slug": "habit",
13
- "displayName": "Habit",
14
- "icon": "repeat",
15
- "color": "#10B981",
16
- "description": "Habit tracker with frequency and streaks",
17
- "scope": "workspace",
18
- "properties": [
19
- { "slug": "name", "valueType": "string", "required": true, "label": "Habit Name" },
20
- { "slug": "frequency", "valueType": "string", "label": "Frequency", "inputType": "select", "enumValues": ["daily", "weekly", "monthly"] },
21
- { "slug": "streak", "valueType": "number", "label": "Current Streak", "constraints": { "min": 0 } },
22
- { "slug": "category", "valueType": "string", "label": "Category" },
23
- { "slug": "notes", "valueType": "string", "label": "Notes", "inputType": "textarea" }
24
- ]
25
- }
26
- ]
27
- }