@rubytech/create-maxy 1.0.701 → 1.0.703

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 (49) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.d.ts +1 -0
  4. package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.d.ts.map +1 -1
  5. package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.js +11 -3
  6. package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.js.map +1 -1
  7. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +13 -1
  8. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +19 -6
  9. package/payload/platform/plugins/docs/references/deployment.md +4 -1
  10. package/payload/platform/plugins/docs/references/internals.md +2 -0
  11. package/payload/platform/plugins/docs/references/platform.md +2 -1
  12. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -0
  13. package/payload/platform/plugins/linkedin-import/PLUGIN.md +26 -0
  14. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +119 -0
  15. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +162 -0
  16. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +102 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +21 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +50 -0
  20. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +15 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +11 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  27. package/payload/platform/templates/agents/admin/IDENTITY.md +5 -3
  28. package/payload/platform/templates/specialists/agents/database-operator.md +113 -0
  29. package/payload/server/chunk-O2FWENOD.js +11530 -0
  30. package/payload/server/chunk-UFLV7I6N.js +11678 -0
  31. package/payload/server/maxy-edge.js +1 -1
  32. package/payload/server/public/assets/{Checkbox-C24zNZ3g.js → Checkbox-CDUmQ1Bu.js} +1 -1
  33. package/payload/server/public/assets/{admin-nKiYLo-a.js → admin-picYWZfn.js} +2 -2
  34. package/payload/server/public/assets/{data-lJ2vb2jV.js → data-yYbcrFrc.js} +1 -1
  35. package/payload/server/public/assets/{file-16btGXA4.js → file-CzLc4Rvq.js} +1 -1
  36. package/payload/server/public/assets/{graph-gsP1la3h.js → graph-BRzC0ZtS.js} +1 -1
  37. package/payload/server/public/assets/{house-DJ35FtjK.js → house-lM4gLKkH.js} +1 -1
  38. package/payload/server/public/assets/jsx-runtime-I6ZqIGn8.css +1 -0
  39. package/payload/server/public/assets/{public-D8whQhxR.js → public-scZadgzt.js} +1 -1
  40. package/payload/server/public/assets/{share-2-C-ICFdTB.js → share-2-CNdrRWue.js} +1 -1
  41. package/payload/server/public/assets/{useVoiceRecorder-xTGR1bpD.js → useVoiceRecorder-D2kfoqVB.js} +1 -1
  42. package/payload/server/public/assets/{x-BrDQr7Iy.js → x-CsDhB6Vr.js} +1 -1
  43. package/payload/server/public/data.html +6 -6
  44. package/payload/server/public/graph.html +7 -7
  45. package/payload/server/public/index.html +8 -8
  46. package/payload/server/public/public.html +5 -5
  47. package/payload/server/server.js +103 -48
  48. package/payload/server/public/assets/jsx-runtime-n7GjUxnC.css +0 -1
  49. /package/payload/server/public/assets/{jsx-runtime-CHagz7cd.js → jsx-runtime-BK2hplUC.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.701",
3
+ "version": "1.0.703",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -42,7 +42,7 @@
42
42
  "plugins": {
43
43
  "core": ["admin", "memory", "docs", "cloudflare", "anthropic", "workflows", "tasks", "scheduling", "email", "contacts", "projects"],
44
44
  "defaultEnabled": ["business-assistant", "sales"],
45
- "available": ["telegram", "waitlist", "deep-research", "whatsapp", "replicate"],
45
+ "available": ["telegram", "waitlist", "deep-research", "whatsapp", "replicate", "linkedin-import"],
46
46
  "excluded": []
47
47
  }
48
48
  }
@@ -8,6 +8,7 @@ export interface OnboardingState {
8
8
  step6CompletedAt: string | null;
9
9
  step7CompletedAt: string | null;
10
10
  step8CompletedAt: string | null;
11
+ step9CompletedAt: string | null;
11
12
  createdAt: string;
12
13
  updatedAt: string;
13
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../src/lib/onboarding.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAC5D,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAC;KAC/C,CAAC,CAAC;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CA4DlB;AAeD,wBAAsB,kBAAkB,CACtC,cAAc,EAAE,MAAM,WAAW,EACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,CAAC,CA0E1B;AAED,wBAAsB,sBAAsB,CAC1C,cAAc,EAAE,MAAM,WAAW,EACjC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC,CAuC1B"}
1
+ {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../src/lib/onboarding.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAC5D,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAC;KAC/C,CAAC,CAAC;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAoED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CA4DlB;AAgBD,wBAAsB,kBAAkB,CACtC,cAAc,EAAE,MAAM,WAAW,EACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,CAAC,CA4E1B;AAED,wBAAsB,sBAAsB,CAC1C,cAAc,EAAE,MAAM,WAAW,EACjC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC,CA6C1B"}
@@ -11,9 +11,10 @@ const STEP_FIELDS = [
11
11
  "step6CompletedAt",
12
12
  "step7CompletedAt",
13
13
  "step8CompletedAt",
14
+ "step9CompletedAt",
14
15
  ];
15
16
  // neo4j-driver v5 returns Integer objects for integer properties, not native
16
- // JS numbers. This converts them safely (values 0–8 are well within range).
17
+ // JS numbers. This converts them safely (values 0–9 are well within range).
17
18
  function toJsNumber(val) {
18
19
  if (typeof val === "number")
19
20
  return val;
@@ -33,6 +34,7 @@ function recordToState(record) {
33
34
  step6CompletedAt: record.get("step6CompletedAt") ?? null,
34
35
  step7CompletedAt: record.get("step7CompletedAt") ?? null,
35
36
  step8CompletedAt: record.get("step8CompletedAt") ?? null,
37
+ step9CompletedAt: record.get("step9CompletedAt") ?? null,
36
38
  createdAt: record.get("createdAt") ?? new Date().toISOString(),
37
39
  updatedAt: record.get("updatedAt") ?? new Date().toISOString(),
38
40
  };
@@ -135,6 +137,7 @@ const READ_STATE_QUERY = `MATCH (o:OnboardingState {accountId: $accountId})
135
137
  o.step6CompletedAt AS step6CompletedAt,
136
138
  o.step7CompletedAt AS step7CompletedAt,
137
139
  o.step8CompletedAt AS step8CompletedAt,
140
+ o.step9CompletedAt AS step9CompletedAt,
138
141
  o.createdAt AS createdAt,
139
142
  o.updatedAt AS updatedAt`;
140
143
  export async function getOnboardingState(sessionFactory, accountId, accountDir) {
@@ -181,6 +184,7 @@ export async function getOnboardingState(sessionFactory, accountId, accountDir)
181
184
  o.step6CompletedAt = $step6CompletedAt,
182
185
  o.step7CompletedAt = $step7CompletedAt,
183
186
  o.step8CompletedAt = $step8CompletedAt,
187
+ o.step9CompletedAt = $step9CompletedAt,
184
188
  o.createdAt = $now,
185
189
  o.updatedAt = $now
186
190
  RETURN o.currentStep AS currentStep,
@@ -192,6 +196,7 @@ export async function getOnboardingState(sessionFactory, accountId, accountDir)
192
196
  o.step6CompletedAt AS step6CompletedAt,
193
197
  o.step7CompletedAt AS step7CompletedAt,
194
198
  o.step8CompletedAt AS step8CompletedAt,
199
+ o.step9CompletedAt AS step9CompletedAt,
195
200
  o.createdAt AS createdAt,
196
201
  o.updatedAt AS updatedAt`, { accountId, currentStep: migratedStep, now, ...stepProps });
197
202
  // Clean up account.json if migration happened
@@ -205,8 +210,8 @@ export async function getOnboardingState(sessionFactory, accountId, accountDir)
205
210
  }
206
211
  }
207
212
  export async function completeOnboardingStep(sessionFactory, accountId, step) {
208
- if (!Number.isInteger(step) || step < 1 || step > 8) {
209
- throw new Error("Step must be between 1 and 8");
213
+ if (!Number.isInteger(step) || step < 1 || step > 9) {
214
+ throw new Error("Step must be between 1 and 9");
210
215
  }
211
216
  const session = sessionFactory();
212
217
  try {
@@ -227,11 +232,14 @@ export async function completeOnboardingStep(sessionFactory, accountId, step) {
227
232
  o.step6CompletedAt AS step6CompletedAt,
228
233
  o.step7CompletedAt AS step7CompletedAt,
229
234
  o.step8CompletedAt AS step8CompletedAt,
235
+ o.step9CompletedAt AS step9CompletedAt,
230
236
  o.createdAt AS createdAt,
231
237
  o.updatedAt AS updatedAt`, { accountId, step, now });
232
238
  if (result.records.length === 0) {
233
239
  throw new Error("No onboarding state found for this account. Call onboarding-get first.");
234
240
  }
241
+ console.log(`[onboarding-step-complete] accountId=${accountId.slice(0, 8)}… ` +
242
+ `step=${step} completedAt=${now}`);
235
243
  return recordToState(result.records[0]);
236
244
  }
237
245
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../../src/lib/onboarding.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../../src/lib/onboarding.ts"],"names":[],"mappings":"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"}
@@ -10,13 +10,25 @@ Applies when the business owner wants to set up, complete, or update their busin
10
10
  ## When to Activate
11
11
 
12
12
  - Admin asks to set up, edit, or complete their business profile
13
+ - Onboarding step 9 delegates here on first run (no `AdminUser` or `LocalBusiness` node yet)
13
14
  - Session-start graph check shows a `LocalBusiness` node with missing data (no hours, no services, empty description)
15
+ - The graph-write gate rejects a `memory-write` or `memory-update` with `no-admin-user` or `no-local-business` — the error message directs the agent here
14
16
  - Admin asks questions like "update my address", "add our opening hours", "change the phone number"
15
17
 
18
+ ## First-run path (no AdminUser or LocalBusiness yet)
19
+
20
+ When onboarding step 9 invokes this skill, or when the graph-write gate reports `no-admin-user` / `no-local-business`, the graph contains no bootstrap nodes. Create them in this order, each as a separate `memory-write` call:
21
+
22
+ 1. **AdminUser.** Read the admin's `userId` and `name` from `admin-identity` in your system prompt. Call `memory-write` with `labels: ["AdminUser"]`, `properties: { userId, name }`, `scope: "admin"`, `relationships: [{ type: "HAS_ACCOUNT_SCOPE", direction: "outgoing", targetNodeId: "<account-anchor>" }]` — or whatever adjacency convention the current schema requires (grep `AdminUser` in the codebase for a live example if unsure). The `AdminUser` label is on the exempt set, so the gate passes for this write.
23
+ 2. **LocalBusiness.** Ask the user for the business name first — this is the only mandatory property. Call `memory-write` with `labels: ["LocalBusiness"]`, minimal `properties: { name }`, `scope: "shared"`, `relationships: [{ type: "OWNS", direction: "incoming", targetNodeId: "<AdminUser-elementId>" }]`. The `LocalBusiness` label is also exempt.
24
+ 3. **Capture the remaining domains** (address, hours, services, FAQs, brand assets) via `memory-update` on the `LocalBusiness` node and `memory-write` for related nodes (`OpeningHoursSpecification`, `Service`, `FAQPage`, `ImageObject`). With both bootstrap nodes present, the gate passes for all subsequent writes.
25
+
26
+ Adapt the order if the user provides data in a different sequence — the invariant is: `AdminUser` and `LocalBusiness` must exist before any non-exempt write. Confirm each batch with the user before writing.
27
+
16
28
  ## Constraints
17
29
 
18
30
  - **Schema-first.** Before any write, load `memory/references/schema-base.md` via `plugin-read`. If `businessType` is known on the `LocalBusiness` node, also load the matching vertical schema. Confirm which schemas were loaded.
19
- - **Read-first.** Search for the existing `LocalBusiness` node and its related entities before asking the user anything. Do not re-ask for data that is already in the graph — reference it and confirm it is still correct.
31
+ - **Read-first.** Search for the existing `LocalBusiness` node and its related entities before asking the user anything. Do not re-ask for data that is already in the graph — reference it and confirm it is still correct. On first run (no `LocalBusiness` yet), skip the search and proceed with the first-run path above.
20
32
  - **Confirm before writing.** Present each batch of data back to the user and get explicit confirmation before calling `memory-write` or `memory-update`.
21
33
  - **Exact property names.** Use Schema.org camelCase property names exactly as defined in the loaded schema reference. No synonyms, no abbreviations.
22
34
  - **No placeholders.** Never write empty strings, "TBD", or "unknown" as property values — they degrade search quality via poor embeddings. If a value is unknown, skip it.
@@ -1,6 +1,6 @@
1
1
  # Onboarding
2
2
 
3
- Guided first-run setup for new installations. The web UI offers a skip/guided choice before Claude OAuth — if the user skips, defaults are applied server-side for steps 1-6 and the agent resumes from step 7. This runs automatically at every session start when `currentStep` is less than 8. Resume from the first incomplete step — skip any step whose number is at or below `currentStep`.
3
+ Guided first-run setup for new installations. The web UI offers a skip/guided choice before Claude OAuth — if the user skips, defaults are applied server-side for steps 1-6 and the agent resumes from step 7. This runs automatically at every session start when `currentStep` is less than 9. Resume from the first incomplete step — skip any step whose number is at or below `currentStep`.
4
4
 
5
5
  After completing each step, persist progress immediately by calling `onboarding-complete-step` with the step number just completed. This ensures that if the session ends, the next session resumes from the right place.
6
6
 
@@ -97,14 +97,14 @@ Write the admin agent personality. All paths are relative to the account directo
97
97
 
98
98
  First, check whether personalisation is already complete: read `agents/admin/SOUL.md`. If it contains actual business content (not just the `# Soul` header), call `onboarding-complete-step` with step 6 and skip to business onboarding — the personalisation is already in place.
99
99
 
100
- If the admin SOUL file (`agents/admin/SOUL.md`) is empty or missing business content, the user must provide personalisation input through conversation. Product knowledge in the graph is background context — not a substitute for the user describing their business identity and preferences. Ask the user about:
100
+ If the admin SOUL file (`agents/admin/SOUL.md`) is empty or missing content, the user must provide personalisation input through conversation. Ask the user about:
101
101
 
102
- - Their business: who the customers are, current stage, and any relevant context
103
102
  - Tone and personality: how the agent should sound (formal, casual, warm, blunt, etc.), language preferences
103
+ - Working style: how the agent should collaborate — proactive vs. wait-for-instruction, terse vs. explanatory, how to handle uncertainty
104
104
 
105
- Use `memory-search` to check for any business context already in the graph reference it to avoid re-asking what's already known, but still confirm with the user. The conversation shapes the personality; the graph fills in factual detail.
105
+ Do NOT ask about the business itself here customer description, current stage, industry, services, etc. belong in the graph via the `business-profile` skill (step 9), not in SOUL. SOUL is the agent's personality template; asking business-identity questions in step 6 pushes factual data into the wrong layer.
106
106
 
107
- SOUL is personality not a product brochure. Factual business data belongs in the graph. SOUL contains only what the agent needs to *be itself*: the business name, your role within it, tone, language preferences, and working style.
107
+ SOUL contains tone, language, working style, and the agent's role within the business factual business data lives in the graph, not here.
108
108
 
109
109
  Present the admin SOUL via `render-component` with `name: "document-editor"` and `data: { title: "Admin agent personality", content: <the markdown>, filePath: "agents/admin/SOUL.md" }`. The user must approve (or edit and approve) the content before any Write call. When the user approves or edits, you receive `{ action: "approve", content: "<markdown>", filePath: "<path>" }` or `{ action: "save", content: "<markdown>", filePath: "<path>" }`. Write the `content` to the specified `filePath` using the Write tool.
110
110
 
@@ -112,10 +112,11 @@ After the admin SOUL is written and approved, call `onboarding-complete-step` wi
112
112
 
113
113
  **Document ingestion.** If the user uploaded any documents during Step 6 (or earlier in the session), dispatch the `content-producer` subagent to ingest them AFTER calling `onboarding-complete-step` — not before. Use the Agent tool with `run_in_background: true`. The critical path (SOUL file, step completion) must not depend on document ingestion succeeding. If no documents were uploaded, skip this step.
114
114
 
115
- **Next steps.** After completing onboarding, let the user know that everything configured during onboarding — plugins, WiFi, output style, thinking view, timezone, and personality — can be changed at any time through conversation. Then suggest two things the user can do next — both are optional and available whenever they are ready:
115
+ **Next steps.** After completing onboarding, let the user know that everything configured during onboarding — plugins, WiFi, output style, thinking view, timezone, and personality — can be changed at any time through conversation. Then suggest three things the user can do next — all optional and available whenever they are ready:
116
116
 
117
117
  1. **Set up remote access** — Cloudflare Tunnel connects the platform to a custom domain so the user and their customers can reach it from anywhere. Ask to get started.
118
118
  2. **Set up the public-facing agent** — an Anthropic API key lets visitors chat with the business. Ask to get started.
119
+ 3. **Capture the business profile** — identity, address, hours, services, FAQs. This populates the graph so the agent can answer business questions and so public-facing customers get accurate information. Ask to get started.
119
120
 
120
121
  ## Step 7 — Cloudflare Tunnel
121
122
 
@@ -165,3 +166,15 @@ Immediately after navigating, run the `action` from the tool's response: call `b
165
166
  All retry loops re-evaluate using the `action` returned in the most recent response. Never re-call `anthropic-setup` with no arguments to get a fresh action — that restarts from Phase 1 and re-checks the stored key unnecessarily.
166
167
 
167
168
  Do not read any skill files. Do not call any other Anthropic tools except `anthropic-setup`. Do not dispatch specialists. The `anthropic-setup` tool handles the entire flow.
169
+
170
+ ## Step 9 — Business profile
171
+
172
+ *(skip if `currentStep` >= 9)*
173
+
174
+ Populate the business's operational identity in the graph — the admin user node, the `LocalBusiness` node, and its core properties (name, description, address, hours, services). Without this, the graph-write gate refuses any user-domain write, so capturing it now avoids the agent being interrupted mid-task later.
175
+
176
+ Invoke the `business-profile` skill. Follow its first-run path: create the `AdminUser` node (bound to the `userId` from users.json), create the `LocalBusiness` node, collect identity + address + whichever additional domains (hours, services, FAQs, brand assets) the user provides. The skill knows how to adapt — accept partial input and allow skipping sections.
177
+
178
+ When `business-profile` reports that the `AdminUser` and `LocalBusiness` nodes exist in the graph, call `onboarding-complete-step` with step 9. Do not mark step 9 complete before both nodes exist — the gate's precondition must be real, not just recorded.
179
+
180
+ If the user declines to complete business-profile now, leave step 9 incomplete. The next session will resume here, and any attempt to write user-domain data will surface `Write blocked (no-admin-user)` or `Write blocked (no-local-business)` via the gate, pulling the agent back into this step.
@@ -65,7 +65,10 @@ The logs will show which service failed to start and why. Common causes:
65
65
  - **Neo4j not started** — run `sudo systemctl start neo4j` and retry
66
66
  - **Port 19200 already in use** — check for another process: `lsof -i :19200`
67
67
  - **Claude OAuth expired** — the next admin session will prompt you to re-authenticate
68
- - **NEO4J_URI port mismatch** — the admin agent throws `port :X disagrees with brand.json neo4jPort :Y`. Edit `NEO4J_URI` in `~/{configDir}/.env` to match the brand's declared `neo4jPort`, or correct `neo4jPort` in `brand.json` and reinstall. (Task 681)
68
+ - **NEO4J_URI guard throws** — the admin agent probes device reality at boot and fails closed on three shapes (Task 682, succeeding Task 681):
69
+ - `no Neo4j listening on [ports]` — nothing is bound; start `neo4j.service` or `neo4j-<brand>.service`, or edit `NEO4J_URI` to a port a Neo4j is actually running on.
70
+ - `port :X not listening; only :Y is live` — single-brand device where `.env` names a port the local Neo4j isn't bound to; edit `NEO4J_URI` in `~/{configDir}/.env` to match the live port (shown in the `[neo4j-probe] listening=[…]` log line).
71
+ - `port :X disagrees with brand.json neo4jPort :Y` — co-tenant device (2+ Neo4js listening) where `.env` names the other brand's port; edit `NEO4J_URI` to match `brand.neo4jPort`, or correct `neo4jPort` in `brand.json` and reinstall. Preserves the Task 577 orphan-write protection on multi-brand devices.
69
72
 
70
73
  ## Systemd units on each device
71
74
 
@@ -315,6 +315,8 @@ This tool is read-only and available to both public and admin agents.
315
315
 
316
316
  `:Conversation` nodes on webchat (admin login, "New conversation" in the burger, a new public visitor) are created lazily. Opening the chat or logging in does not write anything to the graph — Maxy only records the conversation once the user sends a second message. This keeps `conversation-search` and the Conversations modal free of one-turn abandoned threads. WhatsApp and Telegram take the opposite posture: a first inbound DM is a committed interaction, so the graph node is created eagerly on message one. See `.docs/web-chat.md` "Deferred conversation persistence (Task 650)" for the full contract.
317
317
 
318
+ Each row in the Conversations modal exposes a `View logs` row-action that opens a popover with three links — **Stream**, **Errors**, **SSE** — each of which targets `/api/admin/logs?type={stream|error|sse}&conversationId={full-id}` in a new tab. The row's 8-char id chip is click-to-copy; hover reveals the full `conversationId` as a tooltip. See `.docs/web-chat.md` "In-chat retrieval" for the route contract and `console.debug` observability (Task 686).
319
+
318
320
  ---
319
321
 
320
322
  ## Context Assembly — How Retrieved Knowledge Reaches the Agent
@@ -33,7 +33,7 @@ Plugins are installed and managed through conversation. You can add marketplace
33
33
 
34
34
  ## Roles
35
35
 
36
- Maxy has four roles it can dispatch for specific tasks — like members of your team. You don't need to configure or manage them — Maxy decides when to use each role and handles everything automatically. You may see activity like "Dispatching personal-assistant..." in the chat timeline when this happens.
36
+ Maxy has five roles it can dispatch for specific tasks — like members of your team. You don't need to configure or manage them — Maxy decides when to use each role and handles everything automatically. You may see activity like "Dispatching personal-assistant..." in the chat timeline when this happens.
37
37
 
38
38
  | Role | What it does |
39
39
  |------|-------------|
@@ -41,6 +41,7 @@ Maxy has four roles it can dispatch for specific tasks — like members of your
41
41
  | Project Manager | Task and project management — creating, tracking, and completing work items |
42
42
  | Research Assistant | Web research, knowledge management, and visual production |
43
43
  | Content Producer | Document ingestion, image generation, and PDF output |
44
+ | Database Operator | External-archive imports (LinkedIn today; other CRM sources as each one ships) and ad-hoc graph tidy-ups — prune orphans, merge duplicates, add edges |
44
45
 
45
46
  Roles are installed during setup and listed when Maxy introduces itself. Some premium plugins add their own specialists (e.g. the writer-craft plugin adds a manuscript reviewer). Roles installed mid-session become active from the next session.
46
47
 
@@ -39,6 +39,7 @@ These are enabled during onboarding and can be added or removed at any time. Som
39
39
  | `whatsapp` | WhatsApp messaging, pairing, and conversation browsing | Personal assistant |
40
40
  | `waitlist` | Waitlist lifecycle — extract sign-ups from conversations, review | — |
41
41
  | `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
42
+ | `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
42
43
 
43
44
  ### Claude Official (marketplace)
44
45
 
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: linkedin-import
3
+ description: "Import a LinkedIn Basic Data Export into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default."
4
+ tools: []
5
+ always: false
6
+ embed: false
7
+ metadata: {"platform":{"optional":true,"pluginKey":"linkedin-import"}}
8
+ ---
9
+
10
+ # LinkedIn Import
11
+
12
+ Ingests a LinkedIn Basic Data Export (unzipped directory of CSVs + subdirectories) into the Maxy Neo4j graph. Skill-only plugin — no MCP server, no admin tools added. The skill runs under the `database-operator` specialist, which owns external-archive ingestion and ad-hoc graph operations.
13
+
14
+ ## When this applies
15
+
16
+ The admin agent delegates to `database-operator` when the operator drops a `Basic_LinkedInDataExport_*` directory (or references one by path) into chat. The specialist runs the skill's archive-owner confirmation flow before any CSV is read, then ingests the CSVs the skill has references for.
17
+
18
+ ## Intra-plugin growth
19
+
20
+ A LinkedIn export contains ~20 CSVs. The skill ships references for `Profile.csv` and `Connections.csv` today — additional CSVs (Messages, Positions, Skills, Endorsements, Recommendations, Articles, Education, Certifications, Invitations, etc.) land as **new reference files inside this same plugin** under `skills/linkedin-import/references/` as each is authored. They do not become new plugins.
21
+
22
+ ## Relationship to other plugins
23
+
24
+ - **contacts** — waitlist-shaped CRM surface. LinkedIn-imported `:Person` nodes are visible via `contact-list`, but ingestion does not go through contact tools.
25
+ - **memory** — the underlying Cypher-write surface used by the skill (`memory-write`, `memory-update`). The skill is parameterised so all writes carry `source='linkedin'` + `createdByAgent='linkedin-import'` for provenance.
26
+ - **database-operator specialist** — owns execution. See [admin/IDENTITY.md](../../../platform/templates/agents/admin/IDENTITY.md) delegation clause.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: linkedin-import
3
+ description: Import a LinkedIn Basic Data Export into a Maxy Neo4j graph. Triggers when the user asks to import a LinkedIn archive, ingest LinkedIn connections/messages/positions into the graph, or mentions a `Basic_LinkedInDataExport_*.zip` folder. One reference per CSV under `references/`; edges come from what each CSV naturally encodes, never from synthetic "anchor" patterns.
4
+ ---
5
+
6
+ # LinkedIn Import
7
+
8
+ Ingests a LinkedIn Basic Data Export (unzipped directory of CSVs + subdirs) into a Maxy Neo4j graph. Every edge written corresponds to a real relationship expressed by the data — not a convenience anchor.
9
+
10
+ ## Archive-owner confirmation (mandatory first step)
11
+
12
+ A LinkedIn archive belongs to exactly one person — the user whose LinkedIn account produced the export. That person is the subject of every row in the archive: every `CONNECTED_ON_LINKEDIN` edge points from them to a contact, every `WORKS_FOR` edge on `Positions.csv` belongs to them, every `HAS_SKILL` edge on `Skills.csv` is theirs.
13
+
14
+ **Who the archive belongs to is not inferred from who runs the skill.** Joel may be ingesting his own archive today, a business partner's archive next week, or both side-by-side. The skill takes the archive owner as an explicit input and refuses to proceed until it is confirmed.
15
+
16
+ The confirmation flow:
17
+
18
+ 1. List every `:AdminUser` in the graph and display each as `{userId, name, createdAt, accountId(s) via ADMIN_OF}`.
19
+ 2. Ask the operator to name the archive owner. Accept one of:
20
+ - An existing AdminUser userId from the list (typical — Joel's own archive, or a partner who is already a Maxy operator).
21
+ - A new external Person identity (not-yet-an-AdminUser) — create a fresh `:Person` with `givenName`, `familyName`, `email`/`telephone` (at least one) and use its `elementId` as the owner anchor. This path covers ingesting a non-operator's archive for reference.
22
+ 3. Echo the chosen owner back verbatim (`userId=<id> name=<name>`) and require explicit yes/no confirmation before any CSV is touched.
23
+ 4. Persist the confirmed owner as `$ownerUserId` (for AdminUser-type owners) or `$ownerPersonId` (for external Person owners) — this parameter flows into every reference.
24
+
25
+ Refusing to assume is load-bearing: a hard-coded "it's whoever's running this" would misattribute a partner's connections to the operator on first re-use.
26
+
27
+ ## Anchor shape
28
+
29
+ When the owner is an AdminUser (the common case):
30
+
31
+ ```
32
+ (:AdminUser {userId: $ownerUserId}) -[:HAS_PROFILE]-> (:UserProfile {accountId, userId})
33
+ ```
34
+
35
+ Both nodes already exist for any Maxy operator — the HAS_PROFILE edge is MERGEd by [`platform/ui/app/lib/neo4j-store.ts`](../../../../ui/app/lib/neo4j-store.ts) at session start. The LinkedIn import updates these existing nodes; it does not create parallel operator identities.
36
+
37
+ - **Identity + account-scoped behaviour** → `:AdminUser` and `:UserProfile` (existing, unchanged).
38
+ - **LinkedIn profile enrichment** (headline, summary, websites, industry, geoLocation) → property updates on `:UserProfile`.
39
+ - **LinkedIn activity edges** (`CONNECTED_ON_LINKEDIN`, `WORKS_FOR`, `ATTENDED`, `HAS_SKILL`, `HOLDS_CREDENTIAL`, `FOLLOWS`, `AUTHORED`, `PARTICIPANT_IN`) → subjects are the `:AdminUser` (device-level facts: "I know this person") or `:UserProfile` (account-scoped facts: "in this brand I am skilled at X"). Each reference states which and why.
40
+
41
+ When the owner is an external Person (non-operator archive), the anchor is the confirmed `:Person` node instead — same edges, different anchor.
42
+
43
+ ## Invariants
44
+
45
+ 1. **Schema first.** The LinkedIn additions (`person_linkedin_url` index, `:Credential` constraint) live in [`platform/neo4j/schema.cypher`](../../../../neo4j/schema.cypher) and are applied by `platform/scripts/seed-neo4j.sh` on every install / upgrade. If running against a Neo4j that hasn't been reseeded since shipping, pipe `schema.cypher` into `cypher-shell` once before starting — every statement is `IF NOT EXISTS`.
46
+ 2. **Owner confirmed first.** No reference runs until `$ownerUserId` (or `$ownerPersonId`) is persisted and echo-confirmed. The reference set is parameterised — no hard-coded owner.
47
+ 3. **Natural edges only.** Every edge written is one the CSV actually expresses. `Connections.csv` encodes "I am connected on LinkedIn to this person" — that becomes `CONNECTED_ON_LINKEDIN`. No synthetic attach-to-owner pattern bolted onto rows that don't describe a relationship to the owner.
48
+ 4. **Reuse Maxy labels.** Schema-extension is last resort. The LinkedIn set maps onto existing labels wherever semantics align:
49
+ - LinkedIn companies → `:Organization` (existing, memory-write registered).
50
+ - LinkedIn schools → `:Organization` with `organizationCategory: 'educational'`.
51
+ - LinkedIn skills → `:DefinedTerm` with `category: 'linkedin-skill'`.
52
+ - LinkedIn articles → `:CreativeWork` with `category: 'linkedin-article'`.
53
+ - LinkedIn recommendations → `:Review`.
54
+ - LinkedIn DMs → `:Message:LinkedInMessage` + `:Conversation:LinkedInConversation` (sublabels on the existing labels, per Task 633 pattern).
55
+ - Certifications → `:Credential` (genuinely new — no existing Maxy label fits `schema:EducationalOccupationalCredential`).
56
+ 5. **Schema-base property names.** `givenName` / `familyName` / `telephone` / `dateSent` — not `firstName` / `phone` / `sentAt`. See [`platform/plugins/memory/references/schema-base.md`](../../../../plugins/memory/references/schema-base.md).
57
+ 6. **Schema-base edge names.** `(:Person)-[:WORKS_FOR]->(:Organization)` for positions. Custom LinkedIn edges (`CONNECTED_ON_LINKEDIN`, `ENDORSED`) only where schema-base has no canonical name.
58
+ 7. **Provenance stamp.** Every new node: `createdByAgent='linkedin-import'`, `createdBySource='linkedin-import'`, `createdBySession=$sessionId` (UUID per skill run), `createdAt=datetime()`, plus `source='linkedin'` as the data-origin marker.
59
+ 8. **Idempotent MERGE.** Re-running any reference after a fresh export updates properties without duplicating nodes.
60
+
61
+ ## Execution model
62
+
63
+ 1. Confirm `schema.cypher` is applied (one-liner: `cypher-shell ... < platform/neo4j/schema.cypher`; safe to re-run).
64
+ 2. Run the owner-confirmation flow, persist `$ownerUserId` / `$ownerPersonId`.
65
+ 3. For each file the operator approves, load its reference, parse the CSV, batch rows (default 500 per tx), execute the reference's Cypher with `$rows` + owner parameter.
66
+ 4. After each file emit `[linkedin-import] file=<name> rows=<n> created=<n> matched=<n> ms=<elapsed>`.
67
+
68
+ ## File roster
69
+
70
+ Each entry maps to exactly one reference. Order reflects when a reference's edges first become meaningful — `profile.md` enriches the owner's UserProfile and runs before any file that queries it.
71
+
72
+ | Order | File | Reference | Edges produced |
73
+ |-------|------|-----------|----------------|
74
+ | 1 | `Profile.csv` | [profile.md](references/profile.md) | enrichment on `:UserProfile` (no new nodes) |
75
+ | 2 | `Email Addresses.csv` | _pending_ | `:UserProfile` ← `.email` / `:AdminUser.emails` array enrichment |
76
+ | 2 | `PhoneNumbers.csv` | _pending_ | `:UserProfile.telephone` enrichment |
77
+ | 2 | `Whatsapp Phone Numbers.csv` | _pending_ | `:UserProfile.whatsappTelephone` enrichment |
78
+ | 3 | `Positions.csv` | _pending_ | `(:UserProfile)-[:WORKS_FOR {title,startDate,endDate}]->(:Organization)` |
79
+ | 3 | `Education.csv` | _pending_ | `(:UserProfile)-[:ATTENDED {degree,startDate,endDate}]->(:Organization {organizationCategory:'educational'})` |
80
+ | 3 | `Certifications.csv` | _pending_ | `(:UserProfile)-[:HOLDS]->(:Credential)` |
81
+ | 3 | `Languages.csv` | _pending_ | `(:UserProfile)-[:SPEAKS {proficiency}]->(:DefinedTerm {category:'language'})` |
82
+ | 3 | `Skills.csv` | _pending_ | `(:UserProfile)-[:HAS_SKILL]->(:DefinedTerm {category:'linkedin-skill'})` |
83
+ | 4 | `Connections.csv` | [connections.md](references/connections.md) | `(:AdminUser)-[:CONNECTED_ON_LINKEDIN {connectedOn}]->(:Person)` + `(:Person)-[:WORKS_FOR {title}]->(:Organization)` |
84
+ | 5 | `Invitations.csv` | _pending_ | `(:AdminUser)-[:INVITED / :INVITED_BY]->(:Person)` |
85
+ | 5 | `Endorsement_Given_Info.csv` | _pending_ | `(:AdminUser)-[:ENDORSED {date}]->(:Person)-[:FOR_SKILL]->(:DefinedTerm)` |
86
+ | 5 | `Endorsement_Received_Info.csv` | _pending_ | `(:Person)-[:ENDORSED {date}]->(:AdminUser)-[:FOR_SKILL]->(:DefinedTerm)` |
87
+ | 5 | `Recommendations_Given.csv` | _pending_ | `(:Review)-[:ABOUT]->(:Person)`, `(:AdminUser)-[:AUTHORED]->(:Review)` |
88
+ | 5 | `Recommendations_Received.csv` | _pending_ | `(:Review)-[:ABOUT]->(:AdminUser)`, `(:Person)-[:AUTHORED]->(:Review)` |
89
+ | 6 | `Company Follows.csv` | _pending_ | `(:AdminUser)-[:FOLLOWS {since}]->(:Organization)` |
90
+ | 6 | `Causes You Care About.csv` | _pending_ | `(:UserProfile)-[:SUPPORTS]->(:DefinedTerm {category:'cause'})` |
91
+ | 6 | `Events.csv` | _pending_ | `(:AdminUser)-[:ATTENDED_EVENT]->(:Event)` |
92
+ | 7 | `messages.csv` | _pending_ | `:Conversation:LinkedInConversation` + threaded `:Message:LinkedInMessage` + `(:Person)-[:PARTICIPANT_IN]->(conv)` |
93
+ | 7 | `guide_messages.csv` | _pending_ | same (likely empty) |
94
+ | 7 | `learning_coach_messages.csv` | _pending_ | same (likely empty) |
95
+ | 7 | `learning_role_play_messages.csv` | _pending_ | same (likely empty) |
96
+ | 8 | `Articles/` | _pending_ | `(:AdminUser)-[:AUTHORED]->(:CreativeWork {category:'linkedin-article'})` |
97
+ | 8 | `Learning.csv` | _pending_ | `(:UserProfile)-[:COMPLETED]->(:Credential)` (LinkedIn Learning courses) |
98
+ | 9 | `Jobs/Job Applications.csv` | _pending_ | `(:AdminUser)-[:APPLIED_TO {date}]->(:Organization)` via job-posting text on the edge |
99
+ | 9 | `Jobs/Saved Jobs.csv` | _pending_ | `(:AdminUser)-[:SAVED_JOB]->(:Organization)` |
100
+ | 9 | `Jobs/Job Seeker Preferences.csv` | _pending_ | `:UserProfile.jobSeekerPreferences` property blob |
101
+ | 9 | `Jobs/Online Job Postings.csv` | _pending_ | `(:AdminUser)-[:POSTED]->(:JobPosting)` (only rows where the operator authored the posting) |
102
+ | 9 | `SavedJobAlerts.csv` | _pending_ | `:UserProfile.savedJobAlerts` property blob |
103
+ | 10 | `Services Marketplace/Providers.csv` | _pending_ | `:UserProfile.servicesOffered` property blob |
104
+ | 10 | `Receipts_v2.csv` | _pending_ | `(:AdminUser)-[:PAID]->(:Invoice)` (LinkedIn subscriptions) |
105
+ | 10 | `Registration.csv` | _pending_ | `:UserProfile.linkedinRegisteredAt` property enrichment |
106
+
107
+ ## Explicitly out of scope
108
+
109
+ - `Ad_Targeting.csv` — LinkedIn-inferred ad segments, no graph value.
110
+ - `Rich_Media.csv` — time-limited signed LinkedIn CDN URLs; files 404 after expiry.
111
+ - `Private_identity_asset.csv` — duplicates `Profile.csv` summary text.
112
+ - `Profile Summary.csv` — empty in this export.
113
+ - `Job Applicant Saved Screening Question Responses.csv` — ephemeral Q&A, no stable identity.
114
+
115
+ Add a new reference if any of these later become useful — nothing silently flows in.
116
+
117
+ ## Natural-edge discipline (why this matters)
118
+
119
+ The rule isn't "every node must have an edge". The rule is "every node must have the edges its source data actually describes, and no synthetic ones bolted on to satisfy a check." A LinkedIn archive satisfies the orphan doctrine trivially: every row expresses a relationship between the archive owner and someone or something else. If a future file turns out to describe entities with no relationship to the owner (an endorsement between two third parties, say), the reference for that file writes the entities with their natural edges and doesn't fabricate a connection to the owner just because the owner supplied the file.
@@ -0,0 +1,162 @@
1
+ # Reference: Connections.csv
2
+
3
+ Creates a `:Person` for every LinkedIn connection, a `:Organization` for every company named on those rows, `[:WORKS_FOR {title}]` edges from connections to their current employer, and `[:CONNECTED_ON_LINKEDIN {connectedOn}]` edges from the archive owner to every connection.
4
+
5
+ Every edge in this reference is expressed by the source CSV: connections are explicitly labelled as "connected to the archive owner", and each connection row explicitly names a current position. Nothing synthetic.
6
+
7
+ ## Source
8
+
9
+ `Connections.csv`.
10
+
11
+ **Parser note — skip the header preamble.** The file begins with three lines LinkedIn injects:
12
+
13
+ ```
14
+ Notes:
15
+ "When exporting your connection data, …"
16
+ <blank line>
17
+ First Name,Last Name,URL,Email Address,Company,Position,Connected On
18
+ ```
19
+
20
+ The real column header is **line 4**. Either skip the first three lines before parsing, or parse with no header and align columns from row 4 onward.
21
+
22
+ ## Columns → properties and edges
23
+
24
+ | CSV column | Maps to |
25
+ |------------|---------|
26
+ | First Name | `Person.givenName` |
27
+ | Last Name | `Person.familyName` |
28
+ | URL | `Person.linkedinUrl` (natural key) |
29
+ | Email Address | `Person.email` (only when non-empty) |
30
+ | Company | `Organization.name` |
31
+ | Position | `[:WORKS_FOR].title` |
32
+ | Connected On | `[:CONNECTED_ON_LINKEDIN].connectedOn` (ISO 8601) |
33
+
34
+ LinkedIn only emits email for connections who opted in, so most rows have a blank email. Write `email` only when non-empty — avoids colliding with `person_email_unique` on empty strings.
35
+
36
+ ## Natural keys
37
+
38
+ | Entity | Key | Why |
39
+ |--------|-----|-----|
40
+ | `:Person` | `linkedinUrl` (indexed in `platform/neo4j/schema.cypher`) | Stable across exports; display-name collisions are frequent |
41
+ | `:Organization` | `(accountId, name)` | Matches existing `memory-write` convention. Schema-base.md requires `accountId` + `name` on every Organization |
42
+
43
+ ## Anchor
44
+
45
+ ```cypher
46
+ MATCH (owner:AdminUser {userId: $ownerUserId})
47
+ ```
48
+
49
+ Resolved at skill start via the owner-confirmation flow. The owner could instead be a `:Person` if the operator confirmed an external-Person anchor; in that case swap the MATCH to `MATCH (owner:Person) WHERE elementId(owner) = $ownerPersonId` and keep the rest identical — the edges are the same regardless.
50
+
51
+ ## Cypher
52
+
53
+ ```cypher
54
+ // Parameters:
55
+ // $ownerUserId — AdminUser.userId of the confirmed archive owner
56
+ // $accountId — Organization accountId scope for this import
57
+ // $sessionId — UUID generated once per skill run
58
+ // $rows — array of objects:
59
+ // {
60
+ // givenName: "Dee",
61
+ // familyName: "Odus",
62
+ // linkedinUrl: "https://www.linkedin.com/in/deeodus",
63
+ // email: null | "someone@example.com",
64
+ // company: null | "Female Founders Fund",
65
+ // title: null | "Partner",
66
+ // connectedOn: "2026-04-23" // ISO 8601, parsed from "23 Apr 2026"
67
+ // }
68
+
69
+ MATCH (owner:AdminUser {userId: $ownerUserId})
70
+ UNWIND $rows AS row
71
+
72
+ // 1. Upsert the connection Person. linkedinUrl is the natural key.
73
+ MERGE (p:Person {linkedinUrl: row.linkedinUrl})
74
+ ON CREATE SET
75
+ p.accountId = $accountId,
76
+ p.source = 'linkedin',
77
+ p.createdByAgent = 'linkedin-import',
78
+ p.createdBySource = 'linkedin-import',
79
+ p.createdBySession= $sessionId,
80
+ p.createdAt = datetime()
81
+ SET
82
+ p.givenName = row.givenName,
83
+ p.familyName= row.familyName,
84
+ p.name = trim(coalesce(row.givenName,'') + ' ' + coalesce(row.familyName,''))
85
+
86
+ // 1a. Email only when non-empty (avoids person_email_unique collisions on empty strings)
87
+ FOREACH (_ IN CASE WHEN row.email IS NOT NULL AND row.email <> '' THEN [1] ELSE [] END |
88
+ SET p.email = row.email
89
+ )
90
+
91
+ // 2. The CONNECTED_ON_LINKEDIN edge is what this CSV means.
92
+ MERGE (owner)-[c:CONNECTED_ON_LINKEDIN]->(p)
93
+ ON CREATE SET
94
+ c.connectedOn = date(row.connectedOn),
95
+ c.source = 'linkedin',
96
+ c.createdAt = datetime()
97
+
98
+ // 3. If the row names a current employer, create the Organization and WORKS_FOR edge.
99
+ // If no company is named, this block no-ops — we do not synthesise one.
100
+ WITH p, row
101
+ WHERE row.company IS NOT NULL AND row.company <> ''
102
+ MERGE (o:Organization {accountId: $accountId, name: trim(row.company)})
103
+ ON CREATE SET
104
+ o.source = 'linkedin',
105
+ o.createdByAgent = 'linkedin-import',
106
+ o.createdBySource = 'linkedin-import',
107
+ o.createdBySession= $sessionId,
108
+ o.createdAt = datetime()
109
+
110
+ MERGE (p)-[w:WORKS_FOR]->(o)
111
+ ON CREATE SET
112
+ w.title = row.title,
113
+ w.source = 'linkedin',
114
+ w.current = true,
115
+ w.createdAt = datetime()
116
+ ON MATCH SET
117
+ w.title = coalesce(row.title, w.title)
118
+ ```
119
+
120
+ ## Edge semantics — why these and no others
121
+
122
+ - **`(owner)-[:CONNECTED_ON_LINKEDIN]->(:Person)`** — each row of Connections.csv is a declaration that the archive owner and this person are LinkedIn connections. That's the edge.
123
+ - **`(:Person)-[:WORKS_FOR]->(:Organization)`** — each row's Company/Position columns state where the connection currently works. That's the edge. Schema-base calls this `WORKS_FOR`.
124
+
125
+ Rows missing a company produce a Person with a `CONNECTED_ON_LINKEDIN` edge and no WORKS_FOR — because the row didn't express a WORKS_FOR relationship. No placeholder Organization gets minted.
126
+
127
+ Rows missing a position but present with a company produce a `WORKS_FOR` edge with null `title` — the connection works there, we just don't know their role.
128
+
129
+ ## Date parsing
130
+
131
+ `Connected On` arrives as `"23 Apr 2026"`. Convert to ISO 8601 (`2026-04-23`) in the parser before passing to Cypher — `date("2026-04-23")` is Neo4j-native.
132
+
133
+ ## Expected shape
134
+
135
+ - ~3,000–10,000 rows typical for a long-running account.
136
+ - 500 rows per transaction. Single UNWIND handles this; `apoc.periodic.iterate` not required.
137
+
138
+ ## Post-import verification
139
+
140
+ ```cypher
141
+ // Owner → connections count
142
+ MATCH (owner:AdminUser {userId: $ownerUserId})-[:CONNECTED_ON_LINKEDIN]->(p:Person)
143
+ RETURN count(p) AS connections;
144
+
145
+ // LinkedIn-origin organizations count
146
+ MATCH (o:Organization {accountId: $accountId, source: 'linkedin'})
147
+ RETURN count(o) AS organizations;
148
+
149
+ // Spot-check: who works at Female Founders Fund?
150
+ MATCH (o:Organization {accountId: $accountId, name: 'Female Founders Fund'})
151
+ <-[:WORKS_FOR]-(p:Person)
152
+ RETURN p.name, p.linkedinUrl;
153
+ ```
154
+
155
+ ## Failure modes
156
+
157
+ | Symptom | Cause | Fix |
158
+ |---------|-------|-----|
159
+ | Every row parsed as "Notes:,NaN,…" | Header preamble not skipped | Skip first 3 lines before the CSV parser |
160
+ | Constraint violation on `person_email_unique` | Empty email cells treated as `""` instead of `null` | Ensure the parser converts blanks to `null` |
161
+ | `MATCH (owner …)` returns zero rows | `$ownerUserId` invalid — owner-confirmation not run, or operator typed the wrong id | Re-run owner confirmation |
162
+ | `WORKS_FOR` count « connection count | Many rows have blank company | Expected — LinkedIn doesn't force connections to list a current employer |