@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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.d.ts +1 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.js +11 -3
- package/payload/platform/plugins/admin/mcp/dist/lib/onboarding.js.map +1 -1
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +13 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +19 -6
- package/payload/platform/plugins/docs/references/deployment.md +4 -1
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/docs/references/platform.md +2 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -0
- package/payload/platform/plugins/linkedin-import/PLUGIN.md +26 -0
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +119 -0
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +162 -0
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +102 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +21 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +50 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +15 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +11 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +5 -3
- package/payload/platform/templates/specialists/agents/database-operator.md +113 -0
- package/payload/server/chunk-O2FWENOD.js +11530 -0
- package/payload/server/chunk-UFLV7I6N.js +11678 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{Checkbox-C24zNZ3g.js → Checkbox-CDUmQ1Bu.js} +1 -1
- package/payload/server/public/assets/{admin-nKiYLo-a.js → admin-picYWZfn.js} +2 -2
- package/payload/server/public/assets/{data-lJ2vb2jV.js → data-yYbcrFrc.js} +1 -1
- package/payload/server/public/assets/{file-16btGXA4.js → file-CzLc4Rvq.js} +1 -1
- package/payload/server/public/assets/{graph-gsP1la3h.js → graph-BRzC0ZtS.js} +1 -1
- package/payload/server/public/assets/{house-DJ35FtjK.js → house-lM4gLKkH.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-I6ZqIGn8.css +1 -0
- package/payload/server/public/assets/{public-D8whQhxR.js → public-scZadgzt.js} +1 -1
- package/payload/server/public/assets/{share-2-C-ICFdTB.js → share-2-CNdrRWue.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-xTGR1bpD.js → useVoiceRecorder-D2kfoqVB.js} +1 -1
- package/payload/server/public/assets/{x-BrDQr7Iy.js → x-CsDhB6Vr.js} +1 -1
- package/payload/server/public/data.html +6 -6
- package/payload/server/public/graph.html +7 -7
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +103 -48
- package/payload/server/public/assets/jsx-runtime-n7GjUxnC.css +0 -1
- /package/payload/server/public/assets/{jsx-runtime-CHagz7cd.js → jsx-runtime-BK2hplUC.js} +0 -0
package/package.json
CHANGED
|
@@ -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
|
}
|
|
@@ -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;
|
|
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–
|
|
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 >
|
|
209
|
-
throw new Error("Step must be between 1 and
|
|
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":"AAAA,gEAAgE;AAChE,uEAAuE;AAEvE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md
ADDED
|
@@ -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 |
|