linkedin-automation-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Welcome Message
|
|
3
|
+
description: Initial outreach message to new connections
|
|
4
|
+
category: welcome
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Hi {{firstName}},
|
|
8
|
+
|
|
9
|
+
Thanks for connecting! I noticed you're working at {{company}} - that's really interesting work.
|
|
10
|
+
|
|
11
|
+
I'd love to learn more about what you're currently focused on. Are you open to a brief chat sometime this week?
|
|
12
|
+
|
|
13
|
+
Best regards,
|
|
14
|
+
{{senderName}}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
// Mock the paths module
|
|
7
|
+
vi.mock('../utils/paths', () => {
|
|
8
|
+
const testDir = path.join(os.tmpdir(), 'linkedin-cli-templates-test');
|
|
9
|
+
return {
|
|
10
|
+
getConfigDir: () => testDir,
|
|
11
|
+
getSessionsDir: () => path.join(testDir, 'sessions'),
|
|
12
|
+
getTemplatesDir: () => path.join(testDir, 'templates'),
|
|
13
|
+
getDefaultsDir: () => path.join(testDir, 'templates', 'defaults'),
|
|
14
|
+
getCustomDir: () => path.join(testDir, 'templates', 'custom'),
|
|
15
|
+
getConfigFile: () => path.join(testDir, 'config.json'),
|
|
16
|
+
getLogFile: () => path.join(testDir, 'audit.log'),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
import { TemplateEngine, getTemplateEngine } from './engine';
|
|
21
|
+
|
|
22
|
+
describe('TemplateEngine', () => {
|
|
23
|
+
let engine: TemplateEngine;
|
|
24
|
+
const testDir = path.join(os.tmpdir(), 'linkedin-cli-templates-test');
|
|
25
|
+
const defaultsDir = path.join(testDir, 'templates', 'defaults');
|
|
26
|
+
const customDir = path.join(testDir, 'templates', 'custom');
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// Clean up test directory
|
|
30
|
+
if (fs.existsSync(testDir)) {
|
|
31
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
engine = new TemplateEngine();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// Clean up after tests
|
|
38
|
+
if (fs.existsSync(testDir)) {
|
|
39
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('load', () => {
|
|
44
|
+
it('should return null for non-existent template', () => {
|
|
45
|
+
const template = engine.load('non-existent');
|
|
46
|
+
expect(template).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should load template from defaults', () => {
|
|
50
|
+
// Create a default template
|
|
51
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(defaultsDir, 'test.txt'), 'Hello {{name}}!');
|
|
53
|
+
|
|
54
|
+
const template = engine.load('test');
|
|
55
|
+
|
|
56
|
+
expect(template).toBeTruthy();
|
|
57
|
+
expect(template?.id).toBe('test');
|
|
58
|
+
expect(template?.content).toBe('Hello {{name}}!');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should prefer custom templates over defaults', () => {
|
|
62
|
+
// Create both default and custom templates
|
|
63
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
64
|
+
fs.mkdirSync(customDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
fs.writeFileSync(path.join(defaultsDir, 'test.txt'), 'Default content');
|
|
67
|
+
fs.writeFileSync(path.join(customDir, 'test.txt'), 'Custom content');
|
|
68
|
+
|
|
69
|
+
const template = engine.load('test');
|
|
70
|
+
|
|
71
|
+
expect(template?.content).toBe('Custom content');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('parseTemplate', () => {
|
|
76
|
+
it('should parse template without frontmatter', () => {
|
|
77
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
78
|
+
fs.writeFileSync(path.join(defaultsDir, 'simple.txt'), 'Hello {{firstName}}!');
|
|
79
|
+
|
|
80
|
+
const template = engine.load('simple');
|
|
81
|
+
|
|
82
|
+
expect(template?.name).toBe('simple');
|
|
83
|
+
expect(template?.content).toBe('Hello {{firstName}}!');
|
|
84
|
+
expect(template?.variables).toContain('firstName');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should parse template with frontmatter', () => {
|
|
88
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(defaultsDir, 'frontmatter.txt'),
|
|
91
|
+
`---
|
|
92
|
+
name: Welcome Template
|
|
93
|
+
description: A welcome message
|
|
94
|
+
category: welcome
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
Hi {{firstName}}, welcome to {{company}}!`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const template = engine.load('frontmatter');
|
|
101
|
+
|
|
102
|
+
expect(template?.name).toBe('Welcome Template');
|
|
103
|
+
expect(template?.description).toBe('A welcome message');
|
|
104
|
+
expect(template?.category).toBe('welcome');
|
|
105
|
+
expect(template?.variables).toEqual(['firstName', 'company']);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should extract unique variables', () => {
|
|
109
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
110
|
+
fs.writeFileSync(path.join(defaultsDir, 'multi-var.txt'), '{{name}} {{name}} {{other}}');
|
|
111
|
+
|
|
112
|
+
const template = engine.load('multi-var');
|
|
113
|
+
|
|
114
|
+
expect(template?.variables).toEqual(['name', 'other']);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('save', () => {
|
|
119
|
+
it('should save custom template', () => {
|
|
120
|
+
const template = {
|
|
121
|
+
id: 'custom-test',
|
|
122
|
+
name: 'Custom Test',
|
|
123
|
+
description: 'Test template',
|
|
124
|
+
content: 'Hello {{name}}!',
|
|
125
|
+
variables: ['name'],
|
|
126
|
+
category: 'custom' as const,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
engine.save(template);
|
|
130
|
+
|
|
131
|
+
const loaded = engine.load('custom-test');
|
|
132
|
+
expect(loaded?.name).toBe('Custom Test');
|
|
133
|
+
expect(loaded?.content).toBe('Hello {{name}}!');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('delete', () => {
|
|
138
|
+
it('should delete custom template', () => {
|
|
139
|
+
fs.mkdirSync(customDir, { recursive: true });
|
|
140
|
+
fs.writeFileSync(path.join(customDir, 'to-delete.txt'), 'Content to delete');
|
|
141
|
+
|
|
142
|
+
const deleted = engine.delete('to-delete');
|
|
143
|
+
|
|
144
|
+
expect(deleted).toBe(true);
|
|
145
|
+
expect(engine.load('to-delete')).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return false for non-existent template', () => {
|
|
149
|
+
const deleted = engine.delete('non-existent');
|
|
150
|
+
expect(deleted).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should not delete default templates', () => {
|
|
154
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
155
|
+
fs.writeFileSync(path.join(defaultsDir, 'default.txt'), 'Default content');
|
|
156
|
+
|
|
157
|
+
const deleted = engine.delete('default');
|
|
158
|
+
|
|
159
|
+
expect(deleted).toBe(false);
|
|
160
|
+
expect(engine.load('default')).toBeTruthy();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('list', () => {
|
|
165
|
+
it('should return empty array when no templates exist', () => {
|
|
166
|
+
const templates = engine.list();
|
|
167
|
+
expect(templates).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should list all templates', () => {
|
|
171
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
172
|
+
fs.writeFileSync(path.join(defaultsDir, 'template1.txt'), 'Content 1');
|
|
173
|
+
fs.writeFileSync(path.join(defaultsDir, 'template2.txt'), 'Content 2');
|
|
174
|
+
|
|
175
|
+
const templates = engine.list();
|
|
176
|
+
|
|
177
|
+
expect(templates.length).toBe(2);
|
|
178
|
+
expect(templates.map((t) => t.id)).toEqual(
|
|
179
|
+
expect.arrayContaining(['template1', 'template2'])
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('render', () => {
|
|
185
|
+
it('should render template with variables', () => {
|
|
186
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
187
|
+
fs.writeFileSync(
|
|
188
|
+
path.join(defaultsDir, 'render-test.txt'),
|
|
189
|
+
'Hello {{firstName}} {{lastName}}!'
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const result = engine.render('render-test', {
|
|
193
|
+
variables: {
|
|
194
|
+
firstName: 'John',
|
|
195
|
+
lastName: 'Doe',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result).toBe('Hello John Doe!');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should use fallback for missing variables', () => {
|
|
203
|
+
fs.mkdirSync(defaultsDir, { recursive: true });
|
|
204
|
+
fs.writeFileSync(path.join(defaultsDir, 'fallback-test.txt'), 'Hello {{firstName}}!');
|
|
205
|
+
|
|
206
|
+
const result = engine.render('fallback-test', {
|
|
207
|
+
variables: {},
|
|
208
|
+
fallback: '[NAME]',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(result).toBe('Hello [NAME]!');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should throw for non-existent template', () => {
|
|
215
|
+
expect(() => {
|
|
216
|
+
engine.render('non-existent', { variables: {} });
|
|
217
|
+
}).toThrow('Template "non-existent" not found');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('getTemplateEngine singleton', () => {
|
|
223
|
+
it('should return same instance on multiple calls', () => {
|
|
224
|
+
const engine1 = getTemplateEngine();
|
|
225
|
+
const engine2 = getTemplateEngine();
|
|
226
|
+
expect(engine1).toBe(engine2);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getDefaultsDir, getCustomDir } from '../utils/paths';
|
|
4
|
+
import type { Template } from '../types';
|
|
5
|
+
|
|
6
|
+
export interface RenderOptions {
|
|
7
|
+
variables: Record<string, string>;
|
|
8
|
+
fallback?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class TemplateEngine {
|
|
12
|
+
private defaultsDir: string;
|
|
13
|
+
private customDir: string;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.defaultsDir = getDefaultsDir();
|
|
17
|
+
this.customDir = getCustomDir();
|
|
18
|
+
this.ensureDirectories();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private ensureDirectories(): void {
|
|
22
|
+
if (!fs.existsSync(this.defaultsDir)) {
|
|
23
|
+
fs.mkdirSync(this.defaultsDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
if (!fs.existsSync(this.customDir)) {
|
|
26
|
+
fs.mkdirSync(this.customDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load a template by ID
|
|
32
|
+
*/
|
|
33
|
+
load(templateId: string): Template | null {
|
|
34
|
+
// Try custom templates first (user overrides)
|
|
35
|
+
const customPath = path.join(this.customDir, `${templateId}.txt`);
|
|
36
|
+
if (fs.existsSync(customPath)) {
|
|
37
|
+
return this.parseTemplate(templateId, customPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fall back to defaults
|
|
41
|
+
const defaultPath = path.join(this.defaultsDir, `${templateId}.txt`);
|
|
42
|
+
if (fs.existsSync(defaultPath)) {
|
|
43
|
+
return this.parseTemplate(templateId, defaultPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a template file into a Template object
|
|
51
|
+
*/
|
|
52
|
+
private parseTemplate(id: string, filePath: string): Template {
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
const lines = content.split('\n');
|
|
55
|
+
|
|
56
|
+
// Parse frontmatter if present
|
|
57
|
+
let name = id;
|
|
58
|
+
let description = '';
|
|
59
|
+
let category: Template['category'] = 'custom';
|
|
60
|
+
let variables: string[] = [];
|
|
61
|
+
let templateContent = content;
|
|
62
|
+
|
|
63
|
+
if (lines[0] === '---') {
|
|
64
|
+
const endIndex = lines.indexOf('---', 1);
|
|
65
|
+
if (endIndex !== -1) {
|
|
66
|
+
const frontmatter = lines.slice(1, endIndex).join('\n');
|
|
67
|
+
templateContent = lines.slice(endIndex + 1).join('\n');
|
|
68
|
+
|
|
69
|
+
// Parse YAML-like frontmatter
|
|
70
|
+
for (const line of frontmatter.split('\n')) {
|
|
71
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
const [, key, value] = match;
|
|
74
|
+
switch (key) {
|
|
75
|
+
case 'name':
|
|
76
|
+
name = value;
|
|
77
|
+
break;
|
|
78
|
+
case 'description':
|
|
79
|
+
description = value;
|
|
80
|
+
break;
|
|
81
|
+
case 'category':
|
|
82
|
+
if (['welcome', 'followup', 'meeting', 'custom'].includes(value)) {
|
|
83
|
+
category = value as Template['category'];
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract variables from template content
|
|
93
|
+
const variableMatches = templateContent.match(/\{\{(\w+)\}\}/g);
|
|
94
|
+
if (variableMatches) {
|
|
95
|
+
variables = [...new Set(variableMatches.map((m) => m.replace(/[\{\}]/g, '')))];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
name,
|
|
101
|
+
description,
|
|
102
|
+
content: templateContent.trim(),
|
|
103
|
+
variables,
|
|
104
|
+
category,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Save a custom template
|
|
110
|
+
*/
|
|
111
|
+
save(template: Template): void {
|
|
112
|
+
const filePath = path.join(this.customDir, `${template.id}.txt`);
|
|
113
|
+
|
|
114
|
+
const frontmatter = [
|
|
115
|
+
'---',
|
|
116
|
+
`name: ${template.name}`,
|
|
117
|
+
`description: ${template.description}`,
|
|
118
|
+
`category: ${template.category}`,
|
|
119
|
+
'---',
|
|
120
|
+
].join('\n');
|
|
121
|
+
|
|
122
|
+
const content = `${frontmatter}\n\n${template.content}`;
|
|
123
|
+
fs.writeFileSync(filePath, content);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Delete a custom template
|
|
128
|
+
*/
|
|
129
|
+
delete(templateId: string): boolean {
|
|
130
|
+
const customPath = path.join(this.customDir, `${templateId}.txt`);
|
|
131
|
+
if (fs.existsSync(customPath)) {
|
|
132
|
+
fs.unlinkSync(customPath);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* List all available templates
|
|
140
|
+
*/
|
|
141
|
+
list(): Template[] {
|
|
142
|
+
const templates: Template[] = [];
|
|
143
|
+
|
|
144
|
+
// Load defaults
|
|
145
|
+
if (fs.existsSync(this.defaultsDir)) {
|
|
146
|
+
const defaultFiles = fs.readdirSync(this.defaultsDir).filter((f) => f.endsWith('.txt'));
|
|
147
|
+
for (const file of defaultFiles) {
|
|
148
|
+
const id = path.basename(file, '.txt');
|
|
149
|
+
const template = this.load(id);
|
|
150
|
+
if (template) templates.push(template);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Load customs (may override defaults)
|
|
155
|
+
if (fs.existsSync(this.customDir)) {
|
|
156
|
+
const customFiles = fs.readdirSync(this.customDir).filter((f) => f.endsWith('.txt'));
|
|
157
|
+
for (const file of customFiles) {
|
|
158
|
+
const id = path.basename(file, '.txt');
|
|
159
|
+
const template = this.load(id);
|
|
160
|
+
if (template) {
|
|
161
|
+
// Remove default if exists
|
|
162
|
+
const existingIndex = templates.findIndex((t) => t.id === id);
|
|
163
|
+
if (existingIndex !== -1) {
|
|
164
|
+
templates.splice(existingIndex, 1);
|
|
165
|
+
}
|
|
166
|
+
templates.push(template);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return templates;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Render a template with variables
|
|
176
|
+
*/
|
|
177
|
+
render(templateId: string, options: RenderOptions): string {
|
|
178
|
+
const template = this.load(templateId);
|
|
179
|
+
if (!template) {
|
|
180
|
+
throw new Error(`Template "${templateId}" not found`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let content = template.content;
|
|
184
|
+
|
|
185
|
+
// Replace variables
|
|
186
|
+
for (const [key, value] of Object.entries(options.variables)) {
|
|
187
|
+
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
|
|
188
|
+
content = content.replace(regex, value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle missing variables
|
|
192
|
+
if (options.fallback !== undefined) {
|
|
193
|
+
content = content.replace(/\{\{\s*\w+\s*\}\}/g, options.fallback);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return content;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Singleton instance
|
|
201
|
+
let templateEngine: TemplateEngine | null = null;
|
|
202
|
+
|
|
203
|
+
export function getTemplateEngine(): TemplateEngine {
|
|
204
|
+
if (!templateEngine) {
|
|
205
|
+
templateEngine = new TemplateEngine();
|
|
206
|
+
}
|
|
207
|
+
return templateEngine;
|
|
208
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TemplateEngine, getTemplateEngine, type RenderOptions } from './engine';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/types/index.test.ts
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import type { ProfileData, ProfileExtractionOptions, ProfileExtractionResult } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('Profile Types', () => {
|
|
6
|
+
describe('ProfileData', () => {
|
|
7
|
+
it('should allow valid ProfileData', () => {
|
|
8
|
+
const data: ProfileData = {
|
|
9
|
+
full_name: 'John Doe',
|
|
10
|
+
headline: 'Engineer at Company',
|
|
11
|
+
location: 'San Francisco',
|
|
12
|
+
current_company: 'Company',
|
|
13
|
+
current_title: 'Engineer',
|
|
14
|
+
company_linkedin_url: 'https://linkedin.com/company/x',
|
|
15
|
+
years_experience: 5,
|
|
16
|
+
email: 'john@example.com',
|
|
17
|
+
phone: '+1234567890',
|
|
18
|
+
profile_url: 'https://www.linkedin.com/in/johndoe',
|
|
19
|
+
};
|
|
20
|
+
expect(data.full_name).toBe('John Doe');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should allow null optional fields', () => {
|
|
24
|
+
const data: ProfileData = {
|
|
25
|
+
full_name: 'Jane Doe',
|
|
26
|
+
headline: '',
|
|
27
|
+
location: '',
|
|
28
|
+
current_company: null,
|
|
29
|
+
current_title: null,
|
|
30
|
+
company_linkedin_url: null,
|
|
31
|
+
years_experience: null,
|
|
32
|
+
email: null,
|
|
33
|
+
phone: null,
|
|
34
|
+
profile_url: 'https://www.linkedin.com/in/janedoe',
|
|
35
|
+
};
|
|
36
|
+
expect(data.current_company).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('ProfileExtractionOptions', () => {
|
|
41
|
+
it('should allow valid options with all fields', () => {
|
|
42
|
+
const options: ProfileExtractionOptions = {
|
|
43
|
+
profileUrl: 'https://www.linkedin.com/in/johndoe/',
|
|
44
|
+
includeContact: true,
|
|
45
|
+
timeout: 30000,
|
|
46
|
+
};
|
|
47
|
+
expect(options.profileUrl).toBe('https://www.linkedin.com/in/johndoe/');
|
|
48
|
+
expect(options.includeContact).toBe(true);
|
|
49
|
+
expect(options.timeout).toBe(30000);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should allow options with only required fields', () => {
|
|
53
|
+
const options: ProfileExtractionOptions = {
|
|
54
|
+
profileUrl: 'https://www.linkedin.com/in/janedoe/',
|
|
55
|
+
};
|
|
56
|
+
expect(options.profileUrl).toBe('https://www.linkedin.com/in/janedoe/');
|
|
57
|
+
expect(options.includeContact).toBeUndefined();
|
|
58
|
+
expect(options.timeout).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('ProfileExtractionResult', () => {
|
|
63
|
+
it('should allow successful result with data', () => {
|
|
64
|
+
const result: ProfileExtractionResult = {
|
|
65
|
+
success: true,
|
|
66
|
+
message: 'Profile extracted successfully',
|
|
67
|
+
data: {
|
|
68
|
+
full_name: 'Test User',
|
|
69
|
+
headline: 'Test',
|
|
70
|
+
location: 'Test',
|
|
71
|
+
current_company: null,
|
|
72
|
+
current_title: null,
|
|
73
|
+
company_linkedin_url: null,
|
|
74
|
+
years_experience: null,
|
|
75
|
+
email: null,
|
|
76
|
+
phone: null,
|
|
77
|
+
profile_url: 'https://www.linkedin.com/in/testuser/',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
expect(result.success).toBe(true);
|
|
81
|
+
expect(result.data?.full_name).toBe('Test User');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should allow failed result with error', () => {
|
|
85
|
+
const result: ProfileExtractionResult = {
|
|
86
|
+
success: false,
|
|
87
|
+
message: 'Failed to extract profile',
|
|
88
|
+
error: 'Profile not found',
|
|
89
|
+
};
|
|
90
|
+
expect(result.success).toBe(false);
|
|
91
|
+
expect(result.error).toBe('Profile not found');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Session types
|
|
2
|
+
export interface Session {
|
|
3
|
+
cookies: Array<{
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
path: string;
|
|
8
|
+
expires?: number;
|
|
9
|
+
httpOnly?: boolean;
|
|
10
|
+
secure?: boolean;
|
|
11
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
12
|
+
}>;
|
|
13
|
+
localStorage: Record<string, string>;
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Message types
|
|
18
|
+
export interface Message {
|
|
19
|
+
id: string;
|
|
20
|
+
threadId: string;
|
|
21
|
+
sender: {
|
|
22
|
+
name: string;
|
|
23
|
+
profileUrl?: string;
|
|
24
|
+
isMe: boolean;
|
|
25
|
+
};
|
|
26
|
+
content: string;
|
|
27
|
+
timestamp: Date;
|
|
28
|
+
isRead: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// New message sending types
|
|
32
|
+
export interface SendMessageOptions {
|
|
33
|
+
profileUrl: string;
|
|
34
|
+
text: string;
|
|
35
|
+
dryRun?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SendMessageResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
messageId?: string;
|
|
41
|
+
threadId?: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Thread {
|
|
46
|
+
id: string;
|
|
47
|
+
participants: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
profileUrl?: string;
|
|
50
|
+
}>;
|
|
51
|
+
messages: Message[];
|
|
52
|
+
lastActivity: Date;
|
|
53
|
+
unreadCount: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Template types
|
|
57
|
+
export interface Template {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
content: string;
|
|
62
|
+
variables: string[];
|
|
63
|
+
category: 'welcome' | 'followup' | 'meeting' | 'custom';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Configuration types
|
|
67
|
+
export interface Config {
|
|
68
|
+
headless: boolean;
|
|
69
|
+
rateLimit: number;
|
|
70
|
+
sessionTimeout: number;
|
|
71
|
+
dryRun: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Audit log types
|
|
75
|
+
export interface AuditLog {
|
|
76
|
+
timestamp: Date;
|
|
77
|
+
action: string;
|
|
78
|
+
details: Record<string, unknown>;
|
|
79
|
+
success: boolean;
|
|
80
|
+
error?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Company profile types
|
|
84
|
+
export interface CompanyProfile {
|
|
85
|
+
name: string;
|
|
86
|
+
linkedin_url: string;
|
|
87
|
+
website: string | null;
|
|
88
|
+
industry: string | null;
|
|
89
|
+
company_size: string | null;
|
|
90
|
+
headquarters: string | null;
|
|
91
|
+
founded: string | null;
|
|
92
|
+
specialties: string[] | null;
|
|
93
|
+
type: string | null;
|
|
94
|
+
follower_count: number | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Profile Extraction Types
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Profile data extracted from a LinkedIn profile
|
|
103
|
+
*/
|
|
104
|
+
export interface ProfileData {
|
|
105
|
+
// Top card fields
|
|
106
|
+
full_name: string;
|
|
107
|
+
headline: string;
|
|
108
|
+
location: string;
|
|
109
|
+
|
|
110
|
+
// Experience section
|
|
111
|
+
current_company: string | null;
|
|
112
|
+
current_title: string | null;
|
|
113
|
+
company_linkedin_url: string | null;
|
|
114
|
+
|
|
115
|
+
// Calculated
|
|
116
|
+
years_experience: number | null;
|
|
117
|
+
|
|
118
|
+
// Contact info (optional - requires clicking to reveal)
|
|
119
|
+
email: string | null;
|
|
120
|
+
phone: string | null;
|
|
121
|
+
|
|
122
|
+
// Reference
|
|
123
|
+
profile_url: string; // Canonical URL after LinkedIn redirects
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Options for profile extraction
|
|
128
|
+
*/
|
|
129
|
+
export interface ProfileExtractionOptions {
|
|
130
|
+
profileUrl: string;
|
|
131
|
+
includeContact?: boolean;
|
|
132
|
+
timeout?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Result of profile extraction following existing pattern
|
|
137
|
+
*/
|
|
138
|
+
export interface ProfileExtractionResult {
|
|
139
|
+
success: boolean;
|
|
140
|
+
message: string;
|
|
141
|
+
data?: ProfileData;
|
|
142
|
+
error?: string;
|
|
143
|
+
}
|