the-office-cli 1.0.0__py3-none-any.whl
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.
- the_office_cli/__init__.py +0 -0
- the_office_cli/a2a.py +93 -0
- the_office_cli/cli/__init__.py +0 -0
- the_office_cli/cli/decorators.py +179 -0
- the_office_cli/cli/flags.py +54 -0
- the_office_cli/cli/main.py +211 -0
- the_office_cli/commands/__init__.py +0 -0
- the_office_cli/commands/account.py +137 -0
- the_office_cli/commands/auth.py +234 -0
- the_office_cli/commands/campaign.py +476 -0
- the_office_cli/commands/dashboard.py +146 -0
- the_office_cli/commands/info.py +67 -0
- the_office_cli/commands/lead.py +392 -0
- the_office_cli/commands/linkedin.py +1334 -0
- the_office_cli/commands/outreach.py +65 -0
- the_office_cli/commands/report.py +47 -0
- the_office_cli/commands/settings.py +75 -0
- the_office_cli/commands/task.py +192 -0
- the_office_cli/commands/user.py +315 -0
- the_office_cli/config.py +42 -0
- the_office_cli/main.py +21 -0
- the_office_cli/network/__init__.py +0 -0
- the_office_cli/network/errors.py +82 -0
- the_office_cli/network/session.py +77 -0
- the_office_cli/utils/__init__.py +0 -0
- the_office_cli/utils/output.py +38 -0
- the_office_cli/utils/registry.py +35 -0
- the_office_cli/utils/schema.py +67 -0
- the_office_cli-1.0.0.dist-info/METADATA +405 -0
- the_office_cli-1.0.0.dist-info/RECORD +34 -0
- the_office_cli-1.0.0.dist-info/WHEEL +5 -0
- the_office_cli-1.0.0.dist-info/entry_points.txt +2 -0
- the_office_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- the_office_cli-1.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
the_office_cli/a2a.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# *[Michael hands out business cards at the conference]*
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
a2a.py — The business card.
|
|
5
|
+
|
|
6
|
+
The Agent Card for the A2A protocol. When another agent
|
|
7
|
+
asks "who are you and what can you do?" — this is what
|
|
8
|
+
they get back. Skills are derived from the command
|
|
9
|
+
registry so the card is always in sync with the CLI.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from the_office_cli.config import get_api_url
|
|
13
|
+
from the_office_cli.utils.registry import get_registry
|
|
14
|
+
|
|
15
|
+
PROTOCOL_VERSION = "0.2.1"
|
|
16
|
+
AGENT_VERSION = "1.0.0"
|
|
17
|
+
|
|
18
|
+
def _to_json_schema(fields: dict) -> dict:
|
|
19
|
+
"""Turn the CLI's inline input/output shape into proper
|
|
20
|
+
JSON Schema — required fields lifted to the top."""
|
|
21
|
+
properties: dict = {}
|
|
22
|
+
required: list[str] = []
|
|
23
|
+
for field_name, field_def in fields.items():
|
|
24
|
+
if isinstance(field_def, dict):
|
|
25
|
+
prop = {k: v for k, v in field_def.items() if k != "required"}
|
|
26
|
+
properties[field_name] = prop
|
|
27
|
+
if field_def.get("required"):
|
|
28
|
+
required.append(field_name)
|
|
29
|
+
else:
|
|
30
|
+
properties[field_name] = field_def
|
|
31
|
+
schema = {"type": "object", "properties": properties}
|
|
32
|
+
if required:
|
|
33
|
+
schema["required"] = required
|
|
34
|
+
return schema
|
|
35
|
+
|
|
36
|
+
def _to_skill(config: dict) -> dict:
|
|
37
|
+
"""Turn a registered command into an A2A skill — id,
|
|
38
|
+
name, description, schemas, example invocation."""
|
|
39
|
+
s = config["schema"]
|
|
40
|
+
full_name = config.get("full_name") or (
|
|
41
|
+
f"{config.get('_group', '')} {config['name']}".strip()
|
|
42
|
+
)
|
|
43
|
+
return {
|
|
44
|
+
"id": full_name.replace(" ", "-"),
|
|
45
|
+
"name": full_name.title(),
|
|
46
|
+
"description": config.get("description", ""),
|
|
47
|
+
"tags": [config.get("_group", "")],
|
|
48
|
+
"inputSchema": _to_json_schema(s.get("input", {})),
|
|
49
|
+
"outputSchema": _to_json_schema(s.get("output", {})),
|
|
50
|
+
"examples": [f"the-office {full_name}"],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def _build_card(base_url: str) -> dict:
|
|
54
|
+
"""Print a fresh business card with today's skills."""
|
|
55
|
+
return {
|
|
56
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
57
|
+
"id": "the-office",
|
|
58
|
+
"name": "the-office",
|
|
59
|
+
"description": "The World's Best Sales Agent CLI.",
|
|
60
|
+
"url": base_url,
|
|
61
|
+
"version": AGENT_VERSION,
|
|
62
|
+
"provider": {
|
|
63
|
+
"organization": "The Office",
|
|
64
|
+
},
|
|
65
|
+
"capabilities": {
|
|
66
|
+
"streaming": False,
|
|
67
|
+
"pushNotifications": False,
|
|
68
|
+
"extendedAgentCard": False,
|
|
69
|
+
"cardSigning": False,
|
|
70
|
+
"taskStore": False,
|
|
71
|
+
},
|
|
72
|
+
"interfaces": [
|
|
73
|
+
{"protocolBinding": "JSONRPC", "url": f"{base_url}/rpc"},
|
|
74
|
+
],
|
|
75
|
+
"securitySchemes": {
|
|
76
|
+
"bearerJwt": {
|
|
77
|
+
"type": "http",
|
|
78
|
+
"scheme": "bearer",
|
|
79
|
+
"bearerFormat": "JWT",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"security": [{"bearerJwt": []}],
|
|
83
|
+
"defaultInputModes": ["application/json"],
|
|
84
|
+
"defaultOutputModes": ["application/json"],
|
|
85
|
+
"skills": [_to_skill(cfg) for cfg in get_registry()],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def get_agent_card(base_url: str | None = None) -> dict:
|
|
89
|
+
"""Hand out the business card — URL defaults to the API
|
|
90
|
+
the CLI is currently pointed at."""
|
|
91
|
+
return _build_card(base_url or get_api_url())
|
|
92
|
+
|
|
93
|
+
# Card handed out.
|
|
File without changes
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# *[sets up the projector in the conference room]*
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
decorators.py — Sales training.
|
|
5
|
+
|
|
6
|
+
Before a command hits the floor, it goes through
|
|
7
|
+
training — learns the script (validation), does a
|
|
8
|
+
dry run (preview), and gets cleared for live calls.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from the_office_cli.cli.flags import (
|
|
18
|
+
confirm_option,
|
|
19
|
+
dry_run_option,
|
|
20
|
+
idempotency_key_option,
|
|
21
|
+
schema_option,
|
|
22
|
+
)
|
|
23
|
+
from the_office_cli.network.errors import ApiError
|
|
24
|
+
from the_office_cli.network import session
|
|
25
|
+
from the_office_cli.network.session import set_idempotency_key
|
|
26
|
+
from the_office_cli.utils.output import EXIT_SUCCESS, error_stderr, output_stdout
|
|
27
|
+
from the_office_cli.utils.schema import print_schema
|
|
28
|
+
|
|
29
|
+
def _read_stdin(schema: dict, stdin_error: str) -> dict:
|
|
30
|
+
"""Read the training materials. If the basics are
|
|
31
|
+
missing, you're not ready for the floor."""
|
|
32
|
+
body = session.input_stdin()
|
|
33
|
+
if not body:
|
|
34
|
+
error_stderr("INVALID_INPUT", stdin_error)
|
|
35
|
+
for field_name, field_def in schema.get("input", {}).items():
|
|
36
|
+
if (
|
|
37
|
+
isinstance(field_def, dict)
|
|
38
|
+
and field_def.get("required")
|
|
39
|
+
and field_name not in body
|
|
40
|
+
):
|
|
41
|
+
error_stderr("INVALID_INPUT", f"Missing required field: {field_name}")
|
|
42
|
+
return body
|
|
43
|
+
|
|
44
|
+
def _print_dry_run(command: str, schema: dict, kwargs: dict) -> None:
|
|
45
|
+
"""Roleplay the call without a real client on the
|
|
46
|
+
line — practice round, then stop."""
|
|
47
|
+
click.echo(
|
|
48
|
+
json.dumps(
|
|
49
|
+
{
|
|
50
|
+
"status": "dry_run",
|
|
51
|
+
"command": command,
|
|
52
|
+
"would_call": {
|
|
53
|
+
"method": schema["method"],
|
|
54
|
+
"endpoint": schema["endpoint"],
|
|
55
|
+
},
|
|
56
|
+
"with_options": {
|
|
57
|
+
k: v for k, v in kwargs.items() if v is not None
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
sys.exit(EXIT_SUCCESS)
|
|
63
|
+
|
|
64
|
+
def _require_confirm(command: str) -> None:
|
|
65
|
+
"""Firing a client is permanent. Say it out loud
|
|
66
|
+
first. DELETEs need --confirm."""
|
|
67
|
+
error_stderr("CONFIRMATION_REQUIRED", f"Pass --confirm to execute: {command}")
|
|
68
|
+
|
|
69
|
+
def _execute(fn: Callable, kwargs: dict) -> None:
|
|
70
|
+
"""Training's over. Make the real call. If it lands,
|
|
71
|
+
print the win. If not, file the loss."""
|
|
72
|
+
try:
|
|
73
|
+
result = fn(**kwargs)
|
|
74
|
+
result = _apply_pagination(result, kwargs)
|
|
75
|
+
output_stdout(result)
|
|
76
|
+
except ApiError as e:
|
|
77
|
+
error_stderr(
|
|
78
|
+
e.code,
|
|
79
|
+
str(e),
|
|
80
|
+
exit_code=e.exit_code,
|
|
81
|
+
retry=e.retry,
|
|
82
|
+
retry_after=e.retry_after,
|
|
83
|
+
)
|
|
84
|
+
except SystemExit:
|
|
85
|
+
raise
|
|
86
|
+
except Exception as e:
|
|
87
|
+
error_stderr("UNKNOWN_ERROR", str(e))
|
|
88
|
+
|
|
89
|
+
def _apply_pagination(result: dict, kwargs: dict) -> dict:
|
|
90
|
+
"""When the call returns a long list, add page numbers
|
|
91
|
+
so the caller knows where they are."""
|
|
92
|
+
if "items" not in result:
|
|
93
|
+
return result
|
|
94
|
+
page = kwargs.get("page")
|
|
95
|
+
per_page = kwargs.get("per_page")
|
|
96
|
+
if page is None or per_page is None:
|
|
97
|
+
return result
|
|
98
|
+
items = result["items"]
|
|
99
|
+
result["page"] = page
|
|
100
|
+
result["per_page"] = per_page
|
|
101
|
+
if isinstance(items, list):
|
|
102
|
+
result["has_next"] = len(items) >= per_page
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
def command(
|
|
106
|
+
*,
|
|
107
|
+
name: str,
|
|
108
|
+
description: str,
|
|
109
|
+
schema: dict,
|
|
110
|
+
full_name: str | None = None,
|
|
111
|
+
input_stdin: bool = False,
|
|
112
|
+
stdin_error: str = "Pipe JSON to stdin",
|
|
113
|
+
) -> Callable[[Callable], click.Command]:
|
|
114
|
+
"""Sales training — turn a plain function into a command
|
|
115
|
+
that knows the script and can handle errors."""
|
|
116
|
+
|
|
117
|
+
def decorator(fn: Callable) -> click.Command:
|
|
118
|
+
user_params = list(reversed(getattr(fn, "__click_params__", [])))
|
|
119
|
+
params: list[click.Parameter] = list(user_params)
|
|
120
|
+
|
|
121
|
+
params.append(dry_run_option())
|
|
122
|
+
|
|
123
|
+
method = schema.get("method", "GET")
|
|
124
|
+
if method in ("POST", "PUT"):
|
|
125
|
+
params.append(idempotency_key_option())
|
|
126
|
+
if method == "DELETE":
|
|
127
|
+
params.append(confirm_option())
|
|
128
|
+
|
|
129
|
+
config: dict = {
|
|
130
|
+
"name": name,
|
|
131
|
+
"description": description,
|
|
132
|
+
"schema": schema,
|
|
133
|
+
"full_name": full_name,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
params.append(schema_option(_schema_callback(config)))
|
|
137
|
+
|
|
138
|
+
command_name = full_name or name
|
|
139
|
+
|
|
140
|
+
def callback(**kwargs):
|
|
141
|
+
dry_run = kwargs.pop("dry_run", False)
|
|
142
|
+
confirm = kwargs.pop("confirm", False)
|
|
143
|
+
idempotency_key = kwargs.pop("idempotency_key", None)
|
|
144
|
+
set_idempotency_key(idempotency_key)
|
|
145
|
+
|
|
146
|
+
if input_stdin:
|
|
147
|
+
kwargs["_body"] = _read_stdin(schema, stdin_error)
|
|
148
|
+
|
|
149
|
+
if dry_run:
|
|
150
|
+
_print_dry_run(command_name, schema, kwargs)
|
|
151
|
+
|
|
152
|
+
if method == "DELETE" and not confirm:
|
|
153
|
+
_require_confirm(command_name)
|
|
154
|
+
|
|
155
|
+
_execute(fn, kwargs)
|
|
156
|
+
|
|
157
|
+
cmd = click.Command(
|
|
158
|
+
name=name,
|
|
159
|
+
callback=callback,
|
|
160
|
+
params=params,
|
|
161
|
+
help=description,
|
|
162
|
+
)
|
|
163
|
+
cmd._command_config = config
|
|
164
|
+
return cmd
|
|
165
|
+
|
|
166
|
+
return decorator
|
|
167
|
+
|
|
168
|
+
def _schema_callback(config: dict):
|
|
169
|
+
"""Hand out the product sheet before training starts
|
|
170
|
+
— print the contract and exit."""
|
|
171
|
+
|
|
172
|
+
def cb(ctx, _param, value):
|
|
173
|
+
if not value or ctx.resilient_parsing:
|
|
174
|
+
return
|
|
175
|
+
print_schema(config)
|
|
176
|
+
|
|
177
|
+
return cb
|
|
178
|
+
|
|
179
|
+
# Training complete. You're cleared for the floor.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# *[opens the employee handbook to the rules page]*
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
flags.py — The employee handbook.
|
|
5
|
+
|
|
6
|
+
Standard flags every command carries — schema, dry-run,
|
|
7
|
+
idempotency, confirm. Added at build time so nobody
|
|
8
|
+
has to remember them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
def schema_option(callback) -> click.Option:
|
|
14
|
+
"""--schema — show the command's full contract and exit
|
|
15
|
+
before anything runs."""
|
|
16
|
+
return click.Option(
|
|
17
|
+
["--schema"],
|
|
18
|
+
is_flag=True,
|
|
19
|
+
hidden=True,
|
|
20
|
+
is_eager=True,
|
|
21
|
+
expose_value=False,
|
|
22
|
+
callback=callback,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def dry_run_option() -> click.Option:
|
|
26
|
+
"""--dry-run — rehearse the call without dialing."""
|
|
27
|
+
return click.Option(
|
|
28
|
+
["--dry-run", "dry_run"],
|
|
29
|
+
is_flag=True,
|
|
30
|
+
help="Validate inputs and show what would execute, no API call",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def idempotency_key_option() -> click.Option:
|
|
34
|
+
"""--idempotency-key — a receipt number so retries don't
|
|
35
|
+
send the same thing twice."""
|
|
36
|
+
return click.Option(
|
|
37
|
+
["--idempotency-key", "idempotency_key"],
|
|
38
|
+
hidden=True,
|
|
39
|
+
help=(
|
|
40
|
+
"UUID for safe retries — same key = same result, "
|
|
41
|
+
"prevents double-sends"
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def confirm_option() -> click.Option:
|
|
46
|
+
"""--confirm — required for DELETEs. No accidental
|
|
47
|
+
shredding."""
|
|
48
|
+
return click.Option(
|
|
49
|
+
["--confirm"],
|
|
50
|
+
is_flag=True,
|
|
51
|
+
help="Confirm the destructive operation",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Handbook closed.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# *[taps the directory board by the elevator]*
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
cli/main.py — The sales floor directory.
|
|
5
|
+
|
|
6
|
+
Every command is listed here under its department. Pure
|
|
7
|
+
wiring — no business logic, just which group owns what.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from the_office_cli.commands import (
|
|
15
|
+
account,
|
|
16
|
+
auth,
|
|
17
|
+
campaign,
|
|
18
|
+
dashboard,
|
|
19
|
+
lead,
|
|
20
|
+
linkedin,
|
|
21
|
+
info,
|
|
22
|
+
outreach,
|
|
23
|
+
report,
|
|
24
|
+
settings,
|
|
25
|
+
task,
|
|
26
|
+
user,
|
|
27
|
+
)
|
|
28
|
+
from the_office_cli.utils.registry import register
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
@click.version_option(version("the-office-cli"))
|
|
33
|
+
def cli() -> None:
|
|
34
|
+
"""The Office — AI Sales Agent CLI. JSON in, JSON out."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Auth
|
|
38
|
+
register(cli, auth.login, group_name="auth")
|
|
39
|
+
register(cli, auth.logout, group_name="auth")
|
|
40
|
+
register(cli, auth.whoami, group_name="auth")
|
|
41
|
+
register(cli, auth.create_account, group_name="auth")
|
|
42
|
+
register(cli, auth.fire_drill, group_name="auth")
|
|
43
|
+
|
|
44
|
+
# Campaign
|
|
45
|
+
campaign_group = click.Group("campaign", help="Campaign management (8 commands)")
|
|
46
|
+
register(campaign_group, campaign.list_)
|
|
47
|
+
register(campaign_group, campaign.get_)
|
|
48
|
+
register(campaign_group, campaign.update)
|
|
49
|
+
register(campaign_group, campaign.delete_)
|
|
50
|
+
register(campaign_group, campaign.status)
|
|
51
|
+
|
|
52
|
+
create_group = click.Group("create", help="Create campaign (custom | ai | signals)")
|
|
53
|
+
register(create_group, campaign.create_custom, group_name="campaign")
|
|
54
|
+
register(create_group, campaign.create_ai, group_name="campaign")
|
|
55
|
+
register(create_group, campaign.create_signals, group_name="campaign")
|
|
56
|
+
campaign_group.add_command(create_group)
|
|
57
|
+
|
|
58
|
+
cli.add_command(campaign_group)
|
|
59
|
+
|
|
60
|
+
# Lead
|
|
61
|
+
lead_group = click.Group("lead", help="Lead management (6 commands)")
|
|
62
|
+
register(lead_group, lead.list_)
|
|
63
|
+
register(lead_group, lead.get_)
|
|
64
|
+
register(lead_group, lead.create)
|
|
65
|
+
register(lead_group, lead.update)
|
|
66
|
+
register(lead_group, lead.delete_)
|
|
67
|
+
register(lead_group, lead.batch_create)
|
|
68
|
+
cli.add_command(lead_group)
|
|
69
|
+
|
|
70
|
+
# LinkedIn
|
|
71
|
+
linkedin_group = click.Group("linkedin", help="LinkedIn via Unipile API (20 commands)")
|
|
72
|
+
|
|
73
|
+
register(linkedin_group, linkedin.retrieve_own_profile)
|
|
74
|
+
|
|
75
|
+
# LinkedIn connect account
|
|
76
|
+
linkedin_connect_group = click.Group("connect", help="Connect LinkedIn account")
|
|
77
|
+
register(linkedin_connect_group, linkedin.connect_account, group_name="linkedin")
|
|
78
|
+
linkedin_group.add_command(linkedin_connect_group)
|
|
79
|
+
|
|
80
|
+
# LinkedIn reconnect account
|
|
81
|
+
linkedin_reconnect_group = click.Group("reconnect", help="Reconnect LinkedIn account")
|
|
82
|
+
register(linkedin_reconnect_group, linkedin.reconnect_account, group_name="linkedin")
|
|
83
|
+
linkedin_group.add_command(linkedin_reconnect_group)
|
|
84
|
+
|
|
85
|
+
# LinkedIn check account
|
|
86
|
+
linkedin_check_group = click.Group("check", help="Check LinkedIn connection status")
|
|
87
|
+
register(linkedin_check_group, linkedin.check_account, group_name="linkedin")
|
|
88
|
+
linkedin_group.add_command(linkedin_check_group)
|
|
89
|
+
|
|
90
|
+
# LinkedIn retrieve profile and company
|
|
91
|
+
linkedin_retrieve_group = click.Group("retrieve", help="Retrieve profiles and companies")
|
|
92
|
+
register(linkedin_retrieve_group, linkedin.retrieve_profile, group_name="linkedin")
|
|
93
|
+
register(linkedin_retrieve_group, linkedin.retrieve_company, group_name="linkedin")
|
|
94
|
+
linkedin_group.add_command(linkedin_retrieve_group)
|
|
95
|
+
|
|
96
|
+
# LinkedIn send invitation and message
|
|
97
|
+
linkedin_send_group = click.Group("send", help="Send invitations and messages")
|
|
98
|
+
register(linkedin_send_group, linkedin.send_invite, group_name="linkedin")
|
|
99
|
+
register(linkedin_send_group, linkedin.send_message, group_name="linkedin")
|
|
100
|
+
linkedin_group.add_command(linkedin_send_group)
|
|
101
|
+
|
|
102
|
+
# LinkedIn list relations, chats, and messages
|
|
103
|
+
linkedin_list_group = click.Group("list", help="List connections, chats, and messages")
|
|
104
|
+
register(linkedin_list_group, linkedin.list_relations, group_name="linkedin")
|
|
105
|
+
register(linkedin_list_group, linkedin.list_chats, group_name="linkedin")
|
|
106
|
+
register(linkedin_list_group, linkedin.list_messages, group_name="linkedin")
|
|
107
|
+
|
|
108
|
+
# LinkedIn list user messages
|
|
109
|
+
linkedin_list_user_group = click.Group("user", help="Per-user message history")
|
|
110
|
+
register(linkedin_list_user_group, linkedin.list_user_messages, group_name="linkedin")
|
|
111
|
+
linkedin_list_group.add_command(linkedin_list_user_group)
|
|
112
|
+
|
|
113
|
+
linkedin_group.add_command(linkedin_list_group)
|
|
114
|
+
|
|
115
|
+
# LinkedIn start chat
|
|
116
|
+
linkedin_start_group = click.Group("start", help="Start new conversations")
|
|
117
|
+
register(linkedin_start_group, linkedin.start_chat, group_name="linkedin")
|
|
118
|
+
linkedin_group.add_command(linkedin_start_group)
|
|
119
|
+
|
|
120
|
+
# LinkedIn search parameters
|
|
121
|
+
search_group = click.Group("search", help="LinkedIn search (7 subcommands)")
|
|
122
|
+
register(search_group, linkedin.search_parameters, group_name="linkedin")
|
|
123
|
+
|
|
124
|
+
# LinkedIn search classic leads, accounts, posts, jobs
|
|
125
|
+
classic_group = click.Group("classic", help="Classic LinkedIn search -- leads, accounts, posts, jobs")
|
|
126
|
+
register(classic_group, linkedin.search_classic_leads, group_name="linkedin")
|
|
127
|
+
register(classic_group, linkedin.search_classic_accounts, group_name="linkedin")
|
|
128
|
+
register(classic_group, linkedin.search_classic_posts, group_name="linkedin")
|
|
129
|
+
register(classic_group, linkedin.search_classic_jobs, group_name="linkedin")
|
|
130
|
+
search_group.add_command(classic_group)
|
|
131
|
+
|
|
132
|
+
# LinkedIn search sales navigator leads, accounts
|
|
133
|
+
sales_navigator_group = click.Group("sales", help="Sales Navigator search")
|
|
134
|
+
navigator_group = click.Group("navigator", help="Sales Navigator search -- leads, accounts")
|
|
135
|
+
register(navigator_group, linkedin.search_sales_navigator_leads, group_name="linkedin")
|
|
136
|
+
register(navigator_group, linkedin.search_sales_navigator_accounts, group_name="linkedin")
|
|
137
|
+
sales_navigator_group.add_command(navigator_group)
|
|
138
|
+
search_group.add_command(sales_navigator_group)
|
|
139
|
+
|
|
140
|
+
linkedin_group.add_command(search_group)
|
|
141
|
+
cli.add_command(linkedin_group)
|
|
142
|
+
|
|
143
|
+
# Accounts
|
|
144
|
+
account_group = click.Group("account", help="Target company accounts (3 commands)")
|
|
145
|
+
register(account_group, account.list_)
|
|
146
|
+
register(account_group, account.get_)
|
|
147
|
+
register(account_group, account.delete_)
|
|
148
|
+
cli.add_command(account_group)
|
|
149
|
+
|
|
150
|
+
# Dashboard
|
|
151
|
+
register(dashboard.group, dashboard.funnel)
|
|
152
|
+
register(dashboard.group, dashboard.conversations)
|
|
153
|
+
cli.add_command(dashboard.group)
|
|
154
|
+
|
|
155
|
+
# Report
|
|
156
|
+
report_group = click.Group("report", help="Performance report (1 command)")
|
|
157
|
+
register(report_group, report.generate)
|
|
158
|
+
cli.add_command(report_group)
|
|
159
|
+
|
|
160
|
+
# Outreach
|
|
161
|
+
outreach_group = click.Group("outreach", help="Outreach tracking (1 command)")
|
|
162
|
+
register(outreach_group, outreach.list_)
|
|
163
|
+
cli.add_command(outreach_group)
|
|
164
|
+
|
|
165
|
+
# User
|
|
166
|
+
user_group = click.Group("user", help="Team member management (5 commands)")
|
|
167
|
+
register(user_group, user.list_)
|
|
168
|
+
register(user_group, user.get_)
|
|
169
|
+
register(user_group, user.create)
|
|
170
|
+
register(user_group, user.update)
|
|
171
|
+
register(user_group, user.delete_)
|
|
172
|
+
cli.add_command(user_group)
|
|
173
|
+
|
|
174
|
+
# Settings
|
|
175
|
+
settings_group = click.Group("settings", help="View and update settings (2 commands)")
|
|
176
|
+
register(settings_group, settings.get_)
|
|
177
|
+
register(settings_group, settings.update)
|
|
178
|
+
cli.add_command(settings_group)
|
|
179
|
+
|
|
180
|
+
# Task
|
|
181
|
+
task_group = click.Group("task", help="Celery task triggers and polling (6 commands)")
|
|
182
|
+
|
|
183
|
+
# Task find leads
|
|
184
|
+
task_find_group = click.Group("find", help="Find and source leads")
|
|
185
|
+
register(task_find_group, task.find_leads, group_name="task")
|
|
186
|
+
task_group.add_command(task_find_group)
|
|
187
|
+
|
|
188
|
+
# Task send invites
|
|
189
|
+
task_send_group = click.Group("send", help="Send outreach")
|
|
190
|
+
register(task_send_group, task.send_invites, group_name="task")
|
|
191
|
+
task_group.add_command(task_send_group)
|
|
192
|
+
|
|
193
|
+
# Task follow up
|
|
194
|
+
register(task_group, task.follow_up)
|
|
195
|
+
|
|
196
|
+
# Task sync accepted
|
|
197
|
+
task_sync_group = click.Group("sync", help="Sync LinkedIn data")
|
|
198
|
+
register(task_sync_group, task.sync_accepted, group_name="task")
|
|
199
|
+
register(task_sync_group, task.sync_conversations, group_name="task")
|
|
200
|
+
task_group.add_command(task_sync_group)
|
|
201
|
+
|
|
202
|
+
# Task status
|
|
203
|
+
register(task_group, task.status)
|
|
204
|
+
|
|
205
|
+
cli.add_command(task_group)
|
|
206
|
+
|
|
207
|
+
# Info
|
|
208
|
+
register(cli, info.tour, group_name="info")
|
|
209
|
+
cli.add_command(info.completion)
|
|
210
|
+
|
|
211
|
+
# Directory posted.
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# *[highlights a company name on the whiteboard]*
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
account.py — The target accounts.
|
|
5
|
+
|
|
6
|
+
The companies your signals campaigns are watching.
|
|
7
|
+
List them, pull one up, or remove it from the board.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from the_office_cli.cli.decorators import command
|
|
13
|
+
from the_office_cli.network.session import delete, get
|
|
14
|
+
|
|
15
|
+
@command(
|
|
16
|
+
name="list",
|
|
17
|
+
description="Use this command to list all target accounts tracked by signals campaigns, with their buying signals tracked, findings, citations, and confidence levels. Prefer `lead list` when the user wants leads instead of accounts.",
|
|
18
|
+
schema={
|
|
19
|
+
"endpoint": "/accounts", "method": "GET",
|
|
20
|
+
"input": {
|
|
21
|
+
"page": {"type": "integer", "default": 1},
|
|
22
|
+
"per_page": {"type": "integer", "default": 10000},
|
|
23
|
+
},
|
|
24
|
+
"output": {
|
|
25
|
+
"id": {"type": "integer"},
|
|
26
|
+
"campaign_id": {"type": "integer"},
|
|
27
|
+
"campaign": {"type": "string"},
|
|
28
|
+
"website": {"type": "string", "format": "uri"},
|
|
29
|
+
"company_name": {"type": "string"},
|
|
30
|
+
"logo": {"type": "string", "format": "uri"},
|
|
31
|
+
"description": {"type": "string"},
|
|
32
|
+
"days_back": {"type": "integer"},
|
|
33
|
+
"response": {"type": "string"},
|
|
34
|
+
"citations": {"type": "array"},
|
|
35
|
+
"response_summary": {"type": "string"},
|
|
36
|
+
"created_at": {"type": "string", "format": "date-time"},
|
|
37
|
+
"updated_at": {"type": "string", "format": "date-time"},
|
|
38
|
+
},
|
|
39
|
+
"example": {
|
|
40
|
+
"input": {"page": 1},
|
|
41
|
+
"output": {
|
|
42
|
+
"id": 1,
|
|
43
|
+
"campaign_id": 6,
|
|
44
|
+
"campaign": "Vance Refrigeration Takedown",
|
|
45
|
+
"website": "https://vancerefrigeration.com",
|
|
46
|
+
"company_name": "Vance Refrigeration",
|
|
47
|
+
"logo": "https://logo.clearbit.com/vancerefrigeration.com",
|
|
48
|
+
"description": "Bob Vance, Vance Refrigeration. They keep things cold. We keep things personal.",
|
|
49
|
+
"days_back": 365,
|
|
50
|
+
"response": "Vance Refrigeration is a regional commercial HVAC supplier based in Scranton, PA...",
|
|
51
|
+
"citations": [
|
|
52
|
+
{"url": "https://vancerefrigeration.com/about", "title": "About Vance Refrigeration"},
|
|
53
|
+
],
|
|
54
|
+
"response_summary": "Regional supplier, 50 employees, expanding to Wilkes-Barre.",
|
|
55
|
+
"created_at": "2026-03-15T09:00:00+00:00",
|
|
56
|
+
"updated_at": "2026-04-08T11:30:00+00:00",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
@click.option("--page", type=click.INT, default=1, help="Page number for pagination (default: 1)")
|
|
62
|
+
@click.option("--per-page", "per_page", type=click.INT, default=10000, help="Results per page (default: 10000)")
|
|
63
|
+
def list_(page, per_page, **_):
|
|
64
|
+
return {
|
|
65
|
+
"object": "AccountList",
|
|
66
|
+
"items": get("/accounts", params={"page": page, "per_page": per_page}),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@command(
|
|
70
|
+
name="get",
|
|
71
|
+
description="Use this command to get a target account and its buying signals with findings, citations, and confidence levels.",
|
|
72
|
+
schema={
|
|
73
|
+
"endpoint": "/accounts/<id>", "method": "GET",
|
|
74
|
+
"input": {"id": {"type": "integer", "required": True}},
|
|
75
|
+
"output": {
|
|
76
|
+
"id": {"type": "integer"},
|
|
77
|
+
"campaign_id": {"type": "integer"},
|
|
78
|
+
"website": {"type": "string", "format": "uri"},
|
|
79
|
+
"company_name": {"type": "string"},
|
|
80
|
+
"logo": {"type": "string", "format": "uri"},
|
|
81
|
+
"description": {"type": "string"},
|
|
82
|
+
"days_back": {"type": "integer"},
|
|
83
|
+
"response": {"type": "string"},
|
|
84
|
+
"citations": {"type": "array"},
|
|
85
|
+
"response_gemini": {"type": "string"},
|
|
86
|
+
"citations_gemini": {"type": "array"},
|
|
87
|
+
"response_summary": {"type": "string"},
|
|
88
|
+
"created_at": {"type": "string", "format": "date-time"},
|
|
89
|
+
"updated_at": {"type": "string", "format": "date-time"},
|
|
90
|
+
},
|
|
91
|
+
"example": {
|
|
92
|
+
"input": {"id": 1},
|
|
93
|
+
"output": {
|
|
94
|
+
"id": 1,
|
|
95
|
+
"campaign_id": 6,
|
|
96
|
+
"website": "https://vancerefrigeration.com",
|
|
97
|
+
"company_name": "Vance Refrigeration",
|
|
98
|
+
"logo": "https://logo.clearbit.com/vancerefrigeration.com",
|
|
99
|
+
"description": "Bob Vance, Vance Refrigeration. They keep things cold. We keep things personal.",
|
|
100
|
+
"days_back": 365,
|
|
101
|
+
"response": "Vance Refrigeration is a regional commercial HVAC supplier...",
|
|
102
|
+
"citations": [
|
|
103
|
+
{"url": "https://vancerefrigeration.com/about", "title": "About"},
|
|
104
|
+
],
|
|
105
|
+
"response_gemini": "Based on recent data, Vance Refrigeration has been expanding operations...",
|
|
106
|
+
"citations_gemini": [
|
|
107
|
+
{"url": "https://scrantontimes.com/business/vance-expansion", "title": "Vance announces Wilkes-Barre warehouse"},
|
|
108
|
+
],
|
|
109
|
+
"response_summary": "Bob Vance runs a tight ship. 50 employees, growing 12% YoY. Expanding warehouse operations.",
|
|
110
|
+
"created_at": "2026-03-15T09:00:00+00:00",
|
|
111
|
+
"updated_at": "2026-04-08T11:30:00+00:00",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
@click.option("--id", type=click.INT, required=True)
|
|
117
|
+
def get_(id, **_):
|
|
118
|
+
return {"object": "Account", **get(f"/accounts/{id}")}
|
|
119
|
+
|
|
120
|
+
@command(
|
|
121
|
+
name="delete",
|
|
122
|
+
description="Use this command to permanently delete a target account.",
|
|
123
|
+
schema={
|
|
124
|
+
"endpoint": "/accounts/<id>", "method": "DELETE",
|
|
125
|
+
"input": {"id": {"type": "integer", "required": True}},
|
|
126
|
+
"output": {"message": {"type": "string"}},
|
|
127
|
+
"example": {
|
|
128
|
+
"input": {"id": 2},
|
|
129
|
+
"output": {"message": "Account deleted successfully!"},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
@click.option("--id", type=click.INT, required=True)
|
|
134
|
+
def delete_(id, **_):
|
|
135
|
+
return {"object": "Account", **delete(f"/accounts/{id}")}
|
|
136
|
+
|
|
137
|
+
# Accounts reviewed.
|