@synap-core/workspace-templates 0.32.0 → 0.34.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.
package/README.md CHANGED
@@ -22,7 +22,7 @@ How installs, reconciles and tiers work is in the `synap-market` skill
22
22
  |---|---|---|---|
23
23
  | `src/*.yaml` | `workspace` (and `pack`, for a suite) | Legacy workspace YAML (`meta:` plus body). A thin adapter reads it as `{kind: workspace, meta, spec}` | Yes. 31 entries, CI on every push to main |
24
24
  | `capabilities/<key>.capability.json` | `capability` | Manifest (below). `meta` carries the listing and visibility; `spec` is the CapabilityDefinition | Yes, CI on every push to main (§4) |
25
- | `skills/<slug>/` | `skill` | Either a manifest `<slug>.skill.json` (`creative-director`), or an Agent-Skills folder (`SKILL.md` plus references) with a header `skills/<slug>.catalog.json` beside it | The manifest: yes. A folder: only with its header, and only when it holds no reference files (a skill package carries no documents). Headers decided by the founder on 2026-11-05: `bookmark-enrichment` and `routing-to-channels` public, `mapping-the-server` unlisted (private to the official owner); `reading-client-progress` and the parked `stellar-scf-grants-advisor` stay **staged** |
25
+ | `skills/<slug>/` | `skill` | Either a manifest `<slug>.skill.json` (`creative-director`), or an Agent-Skills folder (`SKILL.md` plus references) with a header `skills/<slug>.catalog.json` beside it. A manifest folder may also hold a source file the skill builds on a computer (`facecam-edit/synap-matte.swift`): never a catalog entry, it reaches the computer through the npm package, pinned by version and sha256 in the skill's body (held equal by `src/facecam-edit.matte.test.ts`) | The manifest: yes. A folder: only with its header, and only when it holds no reference files (a skill package carries no documents). Headers decided by the founder on 2026-11-05: `bookmark-enrichment` and `routing-to-channels` public, `mapping-the-server` unlisted (private to the official owner); `reading-client-progress` and the parked `stellar-scf-grants-advisor` stay **staged** |
26
26
  | `views/<slug>.view.json` | `view` | Manifest; `spec.views` in the canonical shape the CP stores (`{slug, displayName, type, config, scopeProfileSlugs}`) | Category `view`. Three today (`bookmark-views-pack`, `event-views-pack`, `task-views-pack`), moved from the old CP pack fragments on 2026-11-05. CI on every push to main |
27
27
  | `automations/<slug>.automation.json` | `automation` | bridge-setup `flowDefinition` form, plus a header `automations/<slug>.catalog.json` beside it | Only with its header, and only without apply-time placeholders. Headers decided by the founder on 2026-11-05: `bookmark-enrichment` public; `url-bookmark-capture`, `github-pr-events` and the five `ship-a-change-*` unlisted. `mail-feed` stays **staged**: it carries two placeholders bridge-setup fills |
28
28
  | `agents/<slug>.agent.yaml` | `agent` | Manifest. `spec` (`schema/agent.ts`) is what the pod reads as `definition.agent`: `harness` (the `agent_type` join key), `aliases`, `worker`, `tried`, `detect`, `wake` (data: `program` + `args` + `params`, never a shell line; required for a worker only), `practices.skills`, `mcp` (where its program reads MCP servers: `config` under ~/, `format` yaml/json, `key` dot path, `entry`; the CLI writes the pod's server there on `synap agents add`, the pod ignores it), `toolProfile` (`entry` / `builder`) + `toolGroups` (only with `entry`): the MCP tools an agent's first key lists, read by the pod (backend#900). One per program of the backend harness registry, plus `hermes`. CP category `agent`, official vendor only, left out of browse lists (the catalog installs every official agent on every pod, founder decision D8) | Yes, on every push to main, like every other kind (the first publish was the founder's, by hand, on 2026-11-07; `FIRST_PUBLISH_BY_HAND` is empty since) |
@@ -321,6 +321,19 @@
321
321
  "company": "string",
322
322
  "accountId": "string"
323
323
  },
324
+ "inputMap": {
325
+ "params": {
326
+ "fullName": "$.name",
327
+ "company": "$.company",
328
+ "accountId": "$account.id"
329
+ },
330
+ "accounts": {
331
+ "verb": "unipile_list_accounts",
332
+ "id": "id",
333
+ "label": "name",
334
+ "provider": "provider"
335
+ }
336
+ },
324
337
  "code": "// Find ONE person's LinkedIn profile by their full name and company: a STRICT\n// match or none (CRM wave 2, S4b). A pure read: it writes nothing, so it runs at\n// once. Writing the match onto the person (linkedinUrl, linkedinProviderId) is\n// the caller's write, proposed for a person to approve (founder decision 7).\n// STRICT: the result's name equals the full name (case- and accent-insensitive,\n// spaces collapsed), the company is in its headline as whole words (a v2\n// PeopleSearchResult has no company field), and exactly one result is both.\n// None, several, or a third page left unread (so one more could exist): no\n// match, with up to 3 candidates (the closest first) for a person to choose.\n// v2: POST /{account_id}/linkedin/search/people, paged by `cursor` (the reply's\n// next_cursor); the company also narrows the search (advanced_keywords).\nconst accountId = String(args?.accountId ?? '').trim();\nconst fullName = String(args?.fullName ?? '').trim();\nconst company = String(args?.company ?? '').trim();\nif (!accountId) throw new Error('linkedin_find_profile: accountId is required — call unipile_list_accounts first to find yours.');\nif (!fullName) throw new Error('linkedin_find_profile: fullName (the person\\'s first and last name) is required');\nif (!company) throw new Error('linkedin_find_profile: company is required: without it a name alone is never a strict match');\n\nconst fold = (s) => String(s ?? '').normalize('NFD').replace(/\\p{M}+/gu, '').toLowerCase();\nconst nameKey = (s) => fold(s).replace(/\\s+/g, ' ').trim();\nconst words = (s) => fold(s).split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\nconst hasWords = (text, part) => {\n if (!part.length) return false;\n for (let i = 0; i + part.length <= text.length; i++) {\n if (part.every((w, j) => text[i + j] === w)) return true;\n }\n return false;\n};\nconst wanted = nameKey(fullName);\nconst companyWords = words(company);\nconst nameMatches = (p) => nameKey(p?.display_name) === wanted;\nconst companyInHeadline = (p) => hasWords(words(p?.headline), companyWords);\n\nconst MAX_PAGES = 2;\nconst account = encodeURIComponent(accountId);\nconst body = { keywords: fullName, advanced_keywords: { company } };\nconst byId = new Map();\nlet cursor = null;\nlet pages = 0;\ndo {\n const qs = cursor ? `?${new URLSearchParams({ cursor }).toString()}` : '';\n const res = await callProvider('unipile_linkedin', 'POST', `/${account}/linkedin/search/people${qs}`, body);\n if (res?.proposed === true) {\n return { matched: false, reason: 'proposed', proposed: true, proposalId: res?.proposalId ?? null, candidates: [] };\n }\n if (res?.status && (res.status < 200 || res.status >= 300)) throw new Error(`linkedin_find_profile: the LinkedIn search failed (status ${res.status}).`);\n const payload = res?.body ?? res;\n const items = Array.isArray(payload?.data) ? payload.data : [];\n for (const p of items) {\n const key = String(p?.id ?? p?.public_identifier ?? `row-${byId.size}`);\n if (!byId.has(key)) byId.set(key, p);\n }\n pages += 1;\n const next = typeof payload?.next_cursor === 'string' && payload.next_cursor ? payload.next_cursor : null;\n cursor = next && next !== cursor ? next : null;\n} while (cursor && pages < MAX_PAGES);\nconst moreUnread = Boolean(cursor);\n\nconst view = (p) => {\n const publicIdentifier = p?.public_identifier ?? null;\n return {\n name: String(p?.display_name ?? '').trim() || null,\n headline: p?.headline ?? null,\n location: p?.location ?? null,\n providerId: p?.id ?? null,\n publicIdentifier,\n profileUrl: p?.profile_url ?? (publicIdentifier ? `https://www.linkedin.com/in/${publicIdentifier}` : null),\n networkDistance: p?.network_distance ?? null,\n nameMatches: nameMatches(p),\n companyInHeadline: companyInHeadline(p),\n };\n};\nconst rows = [...byId.values()].map(view);\nconst strict = rows.filter((r) => r.nameMatches && r.companyInHeadline);\nconst searched = { fullName, company, results: rows.length, pages, moreUnread };\n\nif (strict.length === 1 && !moreUnread) {\n const profile = strict[0];\n return {\n matched: true,\n profile,\n properties: { linkedinUrl: profile.profileUrl, linkedinProviderId: profile.providerId },\n candidates: [],\n searched,\n };\n}\nconst rank = (r) => (r.nameMatches ? 2 : 0) + (r.companyInHeadline ? 1 : 0);\nconst candidates = rows\n .map((r, i) => ({ r, i }))\n .sort((a, b) => rank(b.r) - rank(a.r) || a.i - b.i)\n .slice(0, 3)\n .map(({ r }) => r);\nconst reason = strict.length > 1 ? 'several' : strict.length === 1 ? 'more_results' : 'none';\nreturn { matched: false, reason, candidates, searched };\n"
325
338
  },
326
339
  {
package/dist/index.js CHANGED
@@ -12302,7 +12302,7 @@ var DATA = {
12302
12302
  "name": "Draft",
12303
12303
  "category": "started",
12304
12304
  "subjectStatus": "In Draft",
12305
- "goal": `Entering Draft waits on the person: this stage's gate asks them to approve the script, and that approval moves the post to In Draft. Build nothing before it passes. Run the creative-director readiness check, then produce the draft in the post's format from the approved script, the brand kit and the approved brand-template (layout and recipe) for the format. A visual or video format is built pass by pass through the edit-loop skill and its engine's skill: carousel, LinkedIn PDF or image post, a layered composition saved on the post (carousel-format; passes layer-structure, layer-visual, layer-polish, an image the last two); deck, PDF document or presentation, Open Design (open-design-edit, same passes); video, HyperFrames. Takes given (the run's takes param, on the run session at metadata.params.takes: comma-separated file ids of recorded footage) mean the RE-EDIT video route, with the facecam-edit skill. A take's transcript is ONE content-asset, Kind script, whose File ID is the take's file; its body holds the readable text under a "## Transcript" heading, then a "## Words" heading followed by exactly one \`\`\`json fenced block with the word-level JSON in HyperFrames' shape [{"text","start","end"}] (seconds; [] until the precise pass fills it). Find it before transcribing: if it exists, reuse it and only fill the word timings; if not, create it and link it to the take's file with a references relation. Never a separate document per take. A VIDEO is built in passes, in this order, each one non-destructive (changing a pass never redoes an approved one): cut and order, background, colour, captions, graphics and animation. Skip a pass the route does not use. A run started in the older order builds the passes it has not done in this order, background first. When a pass starts, declare its slot as yours first (an outcome with the pass's key, kind file, no owner, no ask). After each pass, render its preview into the pod and hand the person ONE slot for it (an outcome) on the same key, layer-<pass> (a video: layer-cut, layer-background, layer-colour, layer-captions or layer-graphics): owner human, ref the preview, ask choose with allowOther, options Approve (recommended), Rework and Use my edit, answered as the edit-loop skill spells out (Approve claims the slot: completeOutput). Build the next pass only after the answer. Use my edit: their edit is the pass; never overwrite or redo their changes. Before each of your passes record a version, work from the diff to the current work (never regenerate from the prompt), never write while a pass is the person's, and acknowledge each answer in the room. With takes, every pass is built in ONE HyperFrames project, the folder edit in the run's work folder (~/synap-work/<this run's session id>/edit), shown live in HyperFrames Studio, whose address is the run's studio outcome, retired (retireOutput studio) whenever Studio stops, as the facecam-edit skill spells out. Every render the person hears, a preview included, carries the cut's audio copied (a remux, never the render's own encode), and a re-encode for size touches the video only, as the facecam-edit skill spells out. A LOOK given (the run's look param, at metadata.params.look: a JSON string {"background": {"assetId","fileId"} or {"mode":"blur"} or null, "colour": {"assetId","fileId","intensity"} or null}, picked in the booth before recording) means the Background pass and the Colour pass START from it, as the facecam-edit skill spells out. Background: matte the person from the raw take with the person's edit_video tool (synap_list_capabilities with intent edit_video), handing it the take's pod file id ({"videoFile": id}; the pod signs it at run time); gate the matte (its frame count and duration match the take, no flicker); defocus the background once; composite with ffmpeg (choke, feather, light wrap); then grain on the background and a light match on the person. The background is the background file (downloaded from the pod by its fileId), or, for mode blur, a blurred copy of the raw take. No edit_video tool, a failed run or a failed gate: matte with npx hyperframes remove-background instead and composite the same way over the same background; that failing too: the raw take, and the pass's question says "background not applied". Colour: download the .cube by its fileId and apply it as the grading block grading.lut (src the .cube, intensity the look's intensity). Each is still its own pass, with its own slot (layer-background, layer-colour) and its own question, asked like every other pass; for these two the options add Remove this pass (the raw take shows through: drop the pass, retire its slot with retireOutput, build the next). A null part of the look leaves its pass as it is without a look. The raw take is never overwritten: a pass writes new files beside it.`,
12305
+ "goal": `Entering Draft waits on the person: this stage's gate asks them to approve the script, and that approval moves the post to In Draft. Build nothing before it passes. Run the creative-director readiness check, then produce the draft in the post's format from the approved script, the brand kit and the approved brand-template (layout and recipe) for the format. A visual or video format is built pass by pass through the edit-loop skill and its engine's skill: carousel, LinkedIn PDF or image post, a layered composition saved on the post (carousel-format; passes layer-structure, layer-visual, layer-polish, an image the last two); deck, PDF document or presentation, Open Design (open-design-edit, same passes); video, HyperFrames. Takes given (the run's takes param, on the run session at metadata.params.takes: comma-separated file ids of recorded footage) mean the RE-EDIT video route, with the facecam-edit skill. A take's transcript is ONE content-asset, Kind script, whose File ID is the take's file; its body holds the readable text under a "## Transcript" heading, then a "## Words" heading followed by exactly one \`\`\`json fenced block with the word-level JSON in HyperFrames' shape [{"text","start","end"}] (seconds; [] until the precise pass fills it). Find it before transcribing: if it exists, reuse it and only fill the word timings; if not, create it and link it to the take's file with a references relation. Never a separate document per take. A VIDEO is built in passes, in this order, each one non-destructive (changing a pass never redoes an approved one): cut and order, background, colour, captions, graphics and animation. Skip a pass the route does not use. A run started in the older order builds the passes it has not done in this order, background first. When a pass starts, declare its slot as yours first (an outcome with the pass's key, kind file, no owner, no ask). After each pass, render its preview into the pod and hand the person ONE slot for it (an outcome) on the same key, layer-<pass> (a video: layer-cut, layer-background, layer-colour, layer-captions or layer-graphics): owner human, ref the preview, ask choose with allowOther, options Approve (recommended), Rework and Use my edit, answered as the edit-loop skill spells out (Approve claims the slot: completeOutput). Build the next pass only after the answer. Use my edit: their edit is the pass; never overwrite or redo their changes. Before each of your passes record a version, work from the diff to the current work (never regenerate from the prompt), never write while a pass is the person's, and acknowledge each answer in the room. With takes, every pass is built in ONE HyperFrames project, the folder edit in the run's work folder (~/synap-work/<this run's session id>/edit), shown live in HyperFrames Studio, whose address is the run's studio outcome, retired (retireOutput studio) whenever Studio stops, as the facecam-edit skill spells out. Every render the person hears, a preview included, carries the cut's audio copied (a remux, never the render's own encode), and a re-encode for size touches the video only, as the facecam-edit skill spells out. A LOOK given (the run's look param, at metadata.params.look: a JSON string {"background": {"assetId","fileId"} or {"mode":"blur"} or null, "colour": {"assetId","fileId","intensity"} or null}, picked in the booth before recording) means the Background pass and the Colour pass START from it, as the facecam-edit skill spells out. Background: matte the raw take with the best matte here, in the skill's order (Mac synap-matte, local RVM, the person's edit_video tool (synap_list_capabilities with intent edit_video) if installed, handing it the take's pod file id ({"videoFile": id}; the pod signs it at run time)); gate the matte (its frame count and duration match the take, no flicker); defocus the background once; composite with ffmpeg (choke, feather, clean edge, light wrap); then grain on the background and a light match on the person. The background is the background file (from the pod by its fileId), or, for mode blur, a blurred copy of the raw take. No matte, a failed run or gate: matte with npx hyperframes remove-background instead and composite the same way over the same background, saying: low quality; that failing too: the raw take, and the pass's question says "background not applied". Colour: download the .cube by its fileId and apply it as the grading block grading.lut (src the .cube, intensity the look's intensity). Each is still its own pass, with its own slot (layer-background, layer-colour) and its own question, asked like every other pass; for these two the options add Remove this pass (the raw take shows through: drop the pass, retire its slot with retireOutput, build the next). A null part of the look leaves its pass as it is without a look. The raw take is never overwritten: a pass writes new files beside it.`,
12306
12306
  "gate": {
12307
12307
  "kind": "human"
12308
12308
  },