@series-a/cli 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +6 -0
- package/README.md +190 -0
- package/bin/advisoros.mjs +2 -0
- package/package.json +60 -0
- package/src/api.mjs +76 -0
- package/src/commands/auth.mjs +87 -0
- package/src/commands/flow.mjs +175 -0
- package/src/commands/resources.mjs +400 -0
- package/src/config.mjs +56 -0
- package/src/index.mjs +29 -0
- package/src/ui.mjs +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @series-a/cli
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/) and uses
|
|
5
|
+
[Changesets](https://github.com/changesets/changesets) to manage releases.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — Initial release
|
|
8
|
+
|
|
9
|
+
- `advisoros login` / `logout` / `whoami` / `profiles` with multi-workspace profile support.
|
|
10
|
+
- `leads` — `list`, `get`, `set-status`, `import` (CSV/JSON/JSONL, auto-chunked), `verify`.
|
|
11
|
+
- `campaigns` — `list`, `show`, `create`, `add-leads`, `launch`, `pause`, `resume`.
|
|
12
|
+
- `sequences` — `list`, `show` (ordered step table).
|
|
13
|
+
- `inbox` — `threads` (read-only).
|
|
14
|
+
- `content` — `list`, `show`, `publish`.
|
|
15
|
+
- `agents` — `list`, `run`, `job`.
|
|
16
|
+
- Global flags: `--json`, `--workspace`, `--profile`, `--api-key`.
|
|
17
|
+
- Programmatic SDK via `@series-a/cli/src/api.mjs`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Copyright (c) AdvisorOS / Series A. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This package is distributed under a proprietary license. Use is permitted only
|
|
4
|
+
by AdvisorOS customers and partners against their own workspace API keys. Do not
|
|
5
|
+
redistribute, sublicense, or use against workspaces you do not own without
|
|
6
|
+
written permission.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @series-a/cli
|
|
2
|
+
|
|
3
|
+
A command-line interface to the AdvisorOS platform — drive leads, campaigns, sequences and agents from your terminal. Built on top of the [`/agent-api`](https://github.com/series-a/advisoros/blob/main/docs/agent-api.md) M2M router, so anything the CLI does is auditable and workspace-scoped.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run without installing — always fetches latest
|
|
11
|
+
npx @series-a/cli@latest --help
|
|
12
|
+
|
|
13
|
+
# Pin a version (recommended for CI)
|
|
14
|
+
npx @series-a/cli@0.1.0 --help
|
|
15
|
+
|
|
16
|
+
# Or install globally
|
|
17
|
+
npm i -g @series-a/cli
|
|
18
|
+
advisoros --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires **Node.js ≥ 20**.
|
|
22
|
+
|
|
23
|
+
> Releases are tagged `cli-vX.Y.Z` in the repo and published to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements). See [`CHANGELOG.md`](./CHANGELOG.md).
|
|
24
|
+
|
|
25
|
+
## 1. Get an API key
|
|
26
|
+
|
|
27
|
+
1. Sign in to AdvisorOS.
|
|
28
|
+
2. Open **Settings → API Keys**.
|
|
29
|
+
3. Click **New key** → choose **Workspace** scope (recommended).
|
|
30
|
+
4. Copy the key — you won't see it again.
|
|
31
|
+
|
|
32
|
+
The **CLI quickstart** card on that page shows a copy-paste `npx` command pre-filled with your workspace ID.
|
|
33
|
+
|
|
34
|
+
> Admin keys (cross-workspace) also work but are intentionally restricted. Default to workspace keys.
|
|
35
|
+
|
|
36
|
+
## 2. Log in
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @series-a/cli@latest login
|
|
40
|
+
# Prompts for API key + workspace ID (the 6-char slug, e.g. dmw9wj)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Multiple workspaces?
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
advisoros login --profile acme # save as "acme"
|
|
47
|
+
advisoros leads list --profile acme # use the named profile
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Config is stored at `~/.advisoros/config.json` with mode `0600`. You can also use env vars: `ADVISOROS_API_KEY`, `ADVISOROS_WORKSPACE_ID`, `ADVISOROS_PROFILE`, `ADVISOROS_BASE_URL`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
advisoros whoami
|
|
54
|
+
advisoros profiles
|
|
55
|
+
advisoros logout --profile acme
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 3. Command reference
|
|
59
|
+
|
|
60
|
+
Global flags (work on every subcommand):
|
|
61
|
+
|
|
62
|
+
| Flag | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `--json` | Emit raw JSON instead of formatted tables. |
|
|
65
|
+
| `-w, --workspace <id>` | Override the workspace from your profile. |
|
|
66
|
+
| `-p, --profile <name>` | Pick a saved profile. |
|
|
67
|
+
| `--api-key <key>` | Override the saved API key. |
|
|
68
|
+
|
|
69
|
+
### Leads
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
advisoros leads list [--status new] [--persona founder] [--search acme] [--limit 50]
|
|
73
|
+
advisoros leads get <id>
|
|
74
|
+
advisoros leads set-status <id> <new|engaged|won|lost>
|
|
75
|
+
advisoros leads import --file leads.csv [--persona founder]
|
|
76
|
+
advisoros leads verify <id>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`leads import` accepts CSV (header row), JSON array, or JSONL. Upserts on `(workspace_id, linkedin_url)`. Bulk requests are auto-chunked at 200/call (server cap: 500/call).
|
|
80
|
+
|
|
81
|
+
### Campaigns
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
advisoros campaigns list [--status active]
|
|
85
|
+
advisoros campaigns show <id>
|
|
86
|
+
advisoros campaigns create --name "Founder Pitch Club" \
|
|
87
|
+
--sequence <sequenceId> \
|
|
88
|
+
--senders acc_a,acc_b \
|
|
89
|
+
--daily-limit 25
|
|
90
|
+
advisoros campaigns add-leads <campaignId> --lead-ids id1,id2,id3
|
|
91
|
+
advisoros campaigns add-leads <campaignId> --file lead-ids.txt
|
|
92
|
+
advisoros campaigns launch <id>
|
|
93
|
+
advisoros campaigns pause <id>
|
|
94
|
+
advisoros campaigns resume <id>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`launch` flips status to `active` — the `enforce_campaign_char_limits` trigger validates every step's copy against LinkedIn limits before allowing the transition.
|
|
98
|
+
|
|
99
|
+
### Sequences / Inbox / Content / Agents
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
advisoros sequences list
|
|
103
|
+
advisoros sequences show <id>
|
|
104
|
+
|
|
105
|
+
advisoros inbox threads [--campaign <id>] [--lead <id>] [--needs-attention] [--limit 50]
|
|
106
|
+
|
|
107
|
+
advisoros content list [--status published] [--type post] [--search ...]
|
|
108
|
+
advisoros content show <id>
|
|
109
|
+
advisoros content publish <id>
|
|
110
|
+
|
|
111
|
+
advisoros agents list
|
|
112
|
+
advisoros agents run <name> --input '{"foo":"bar"}'
|
|
113
|
+
advisoros agents job <jobId>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **Heads-up:** `inbox send`/`reply` are intentionally **not** in v1. Outbound goes through `campaigns launch` (sender resolution + char limits + per-sender daily budget). For one-off DMs, use the web inbox.
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
**Spin up a campaign from a fresh CSV:**
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
advisoros leads import --file ./prospects.csv --persona founder
|
|
124
|
+
advisoros leads list --persona founder --limit 200 --json \
|
|
125
|
+
| jq -r '.leads[].id' > lead-ids.txt
|
|
126
|
+
|
|
127
|
+
advisoros campaigns create --name "Q3 Founder Outreach" \
|
|
128
|
+
--sequence 1a2b3c4d-... --senders acc_milan --daily-limit 25
|
|
129
|
+
# → ✓ Created campaign 8297f04d-...
|
|
130
|
+
|
|
131
|
+
advisoros campaigns add-leads 8297f04d-... --file lead-ids.txt
|
|
132
|
+
advisoros campaigns launch 8297f04d-...
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Tail a long-running agent job:**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
JOB=$(advisoros agents run lead-intelligence --input '{}' --json | jq -r '.run.job_id')
|
|
139
|
+
watch -n 5 "advisoros agents job $JOB --json | jq '{status,progress}'"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Exit codes
|
|
143
|
+
|
|
144
|
+
| Code | Meaning |
|
|
145
|
+
|---|---|
|
|
146
|
+
| 0 | Success |
|
|
147
|
+
| 1 | Generic failure (`error` in JSON response or thrown). Stderr contains the message. |
|
|
148
|
+
|
|
149
|
+
## Programmatic use
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { call } from "@series-a/cli/src/api.mjs";
|
|
153
|
+
|
|
154
|
+
const { campaigns } = await call("campaigns", "list", { status: "active" }, {
|
|
155
|
+
apiKey: process.env.ADVISOROS_API_KEY,
|
|
156
|
+
workspaceId: "dmw9wj",
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Troubleshooting
|
|
161
|
+
|
|
162
|
+
- **401 Unauthorized** — key wrong / revoked / wrong base URL. Re-run `advisoros login`.
|
|
163
|
+
- **403 not authorized for the requested workspace_id** — workspace-scoped key + `--workspace` to a different workspace. Use the matching key.
|
|
164
|
+
- **CHAR_LIMIT_EXCEEDED on launch** — a sequence step exceeds LinkedIn limits (200 invitations / 8000 DMs / 1250 comments). Fix in web app.
|
|
165
|
+
- **Network error** — check `ADVISOROS_BASE_URL` if you're pointing at a non-default deployment.
|
|
166
|
+
|
|
167
|
+
## Releasing (maintainers)
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
cd tools/cli
|
|
171
|
+
npx changeset # describe the change
|
|
172
|
+
npm run version # bump package.json + CHANGELOG.md
|
|
173
|
+
git commit -am "chore(cli): release v$(node -p "require('./package.json').version")"
|
|
174
|
+
git tag "cli-v$(node -p "require('./package.json').version")"
|
|
175
|
+
git push --follow-tags
|
|
176
|
+
# → .github/workflows/cli-release.yml publishes to npm with provenance
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`NPM_TOKEN` must be set in the repo's GitHub Actions secrets (automation token with publish access to the `@series-a` org).
|
|
180
|
+
|
|
181
|
+
## Roadmap (v1.1)
|
|
182
|
+
|
|
183
|
+
- `inbox send` / `inbox reply` with explicit `--confirm` + per-sender budget enforcement.
|
|
184
|
+
- `agents tail <jobId>` (SSE).
|
|
185
|
+
- `admin workspaces …` (admin keys only).
|
|
186
|
+
- Single-binary builds via `pkg`.
|
|
187
|
+
|
|
188
|
+
## Full API reference
|
|
189
|
+
|
|
190
|
+
See [`docs/agent-api.md`](../../docs/agent-api.md) for the underlying `/agent-api` envelope.
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@series-a/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AdvisorOS command-line interface — drive leads, campaigns, sequences and agents from your terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"advisoros": "bin/advisoros.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node bin/advisoros.mjs",
|
|
21
|
+
"smoke": "node bin/advisoros.mjs --help >/dev/null && node bin/advisoros.mjs --version >/dev/null && node -e \"import('node:fs').then(({readFileSync})=>{const s=readFileSync('bin/advisoros.mjs','utf8');if(!s.startsWith('#!/usr/bin/env node')){console.error('Missing shebang in bin/advisoros.mjs');process.exit(1)}})\"",
|
|
22
|
+
"prepublishOnly": "npm run smoke",
|
|
23
|
+
"changeset": "changeset",
|
|
24
|
+
"version": "changeset version",
|
|
25
|
+
"release": "npm run smoke && changeset publish --access public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^12.1.0",
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"cli-table3": "^0.6.5",
|
|
31
|
+
"ora": "^8.1.1",
|
|
32
|
+
"prompts": "^2.4.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@changesets/cli": "^2.27.9"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/series-a/advisoros.git",
|
|
43
|
+
"directory": "tools/cli"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://advisoros.series-a.co",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/series-a/advisoros/issues"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"advisoros",
|
|
51
|
+
"cli",
|
|
52
|
+
"linkedin",
|
|
53
|
+
"campaigns",
|
|
54
|
+
"leads",
|
|
55
|
+
"sequences",
|
|
56
|
+
"agent-api"
|
|
57
|
+
],
|
|
58
|
+
"author": "AdvisorOS",
|
|
59
|
+
"license": "UNLICENSED"
|
|
60
|
+
}
|
package/src/api.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getProfile, DEFAULTS } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
export class AdvisorOSApiError extends Error {
|
|
4
|
+
constructor(message, { status, body } = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AdvisorOSApiError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Invoke the AdvisorOS /agent-api router.
|
|
14
|
+
* Resolves credentials from --profile / env / saved config in that order.
|
|
15
|
+
*/
|
|
16
|
+
export async function call(resource, action, params = {}, opts = {}) {
|
|
17
|
+
const profileName = opts.profile || process.env.ADVISOROS_PROFILE || "default";
|
|
18
|
+
const stored = await getProfile(profileName);
|
|
19
|
+
const apiKey = opts.apiKey || process.env.ADVISOROS_API_KEY || stored?.apiKey;
|
|
20
|
+
const workspaceId =
|
|
21
|
+
opts.workspaceId ||
|
|
22
|
+
process.env.ADVISOROS_WORKSPACE_ID ||
|
|
23
|
+
stored?.workspaceId;
|
|
24
|
+
const baseUrl =
|
|
25
|
+
opts.baseUrl ||
|
|
26
|
+
process.env.ADVISOROS_BASE_URL ||
|
|
27
|
+
stored?.baseUrl ||
|
|
28
|
+
DEFAULTS.baseUrl;
|
|
29
|
+
const agentName = opts.agentName || stored?.agentName || "advisoros-cli";
|
|
30
|
+
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new AdvisorOSApiError(
|
|
33
|
+
"No API key configured. Run `advisoros login` or set ADVISOROS_API_KEY.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
// workspaces resource doesn't require a workspace_id in the envelope.
|
|
37
|
+
if (!workspaceId && resource !== "workspaces") {
|
|
38
|
+
throw new AdvisorOSApiError(
|
|
39
|
+
"No workspace configured. Run `advisoros login` again or pass --workspace.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const body = {
|
|
44
|
+
agent_name: agentName,
|
|
45
|
+
workspace_id: workspaceId ?? "",
|
|
46
|
+
resource,
|
|
47
|
+
action,
|
|
48
|
+
params,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(baseUrl, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"x-service-account-key": apiKey,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
});
|
|
61
|
+
} catch (e) {
|
|
62
|
+
throw new AdvisorOSApiError(`Network error: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
let json;
|
|
67
|
+
try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
|
68
|
+
|
|
69
|
+
if (!res.ok || json?.success === false) {
|
|
70
|
+
throw new AdvisorOSApiError(
|
|
71
|
+
json?.error || `HTTP ${res.status}`,
|
|
72
|
+
{ status: res.status, body: json },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return json;
|
|
76
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getProfile, setProfile, listProfiles, deleteProfile, CONFIG_PATH, DEFAULTS } from "../config.mjs";
|
|
4
|
+
import { call } from "../api.mjs";
|
|
5
|
+
import { emit, fail, ctxFrom, withCommonOpts } from "../ui.mjs";
|
|
6
|
+
|
|
7
|
+
export function registerAuth(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("login")
|
|
10
|
+
.description("Save an API key + workspace to ~/.advisoros/config.json")
|
|
11
|
+
.option("-p, --profile <name>", "profile name", "default")
|
|
12
|
+
.option("--api-key <key>", "API key (otherwise prompted)")
|
|
13
|
+
.option("--workspace <id>", "workspace id (otherwise prompted)")
|
|
14
|
+
.option("--base-url <url>", "agent-api base URL", DEFAULTS.baseUrl)
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
try {
|
|
17
|
+
let { apiKey, workspace, baseUrl, profile } = opts;
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
const r = await prompts({
|
|
20
|
+
type: "password",
|
|
21
|
+
name: "v",
|
|
22
|
+
message: "API key (from Settings → API Keys)",
|
|
23
|
+
validate: (v) => v && v.length > 10 ? true : "Required",
|
|
24
|
+
});
|
|
25
|
+
apiKey = r.v;
|
|
26
|
+
}
|
|
27
|
+
if (!apiKey) return fail(new Error("Aborted"));
|
|
28
|
+
if (!workspace) {
|
|
29
|
+
const r = await prompts({
|
|
30
|
+
type: "text",
|
|
31
|
+
name: "v",
|
|
32
|
+
message: "Workspace ID (6-char slug, e.g. dmw9wj)",
|
|
33
|
+
validate: (v) => v && v.length >= 3 ? true : "Required",
|
|
34
|
+
});
|
|
35
|
+
workspace = r.v;
|
|
36
|
+
}
|
|
37
|
+
if (!workspace) return fail(new Error("Aborted"));
|
|
38
|
+
|
|
39
|
+
// Verify the credentials with a cheap round-trip.
|
|
40
|
+
const verify = await call("agents", "list", {}, {
|
|
41
|
+
apiKey, workspaceId: workspace, baseUrl,
|
|
42
|
+
});
|
|
43
|
+
const count = verify?.agents?.length ?? 0;
|
|
44
|
+
await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl });
|
|
45
|
+
process.stdout.write(chalk.green(
|
|
46
|
+
`\n✓ Logged in as profile "${profile}" — workspace ${workspace} (${count} agents visible)\n` +
|
|
47
|
+
` Config: ${CONFIG_PATH}\n`,
|
|
48
|
+
));
|
|
49
|
+
} catch (e) { fail(e); }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command("whoami")
|
|
54
|
+
.description("Show the active profile and workspace")
|
|
55
|
+
.option("-p, --profile <name>", "profile name", "default")
|
|
56
|
+
.action(async (opts) => {
|
|
57
|
+
try {
|
|
58
|
+
const p = await getProfile(opts.profile);
|
|
59
|
+
if (!p) return fail(new Error(`Profile "${opts.profile}" not found. Run \`advisoros login\`.`));
|
|
60
|
+
const masked = p.apiKey ? p.apiKey.slice(0, 8) + "…" + p.apiKey.slice(-4) : "(none)";
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
`profile: ${opts.profile}\n` +
|
|
63
|
+
`workspace: ${p.workspaceId}\n` +
|
|
64
|
+
`base url: ${p.baseUrl || DEFAULTS.baseUrl}\n` +
|
|
65
|
+
`api key: ${masked}\n`,
|
|
66
|
+
);
|
|
67
|
+
} catch (e) { fail(e); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command("profiles")
|
|
72
|
+
.description("List saved profiles")
|
|
73
|
+
.action(async () => {
|
|
74
|
+
const ps = await listProfiles();
|
|
75
|
+
if (!ps.length) return process.stdout.write("(no profiles — run `advisoros login`)\n");
|
|
76
|
+
for (const name of ps) process.stdout.write(`- ${name}\n`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command("logout")
|
|
81
|
+
.description("Remove a saved profile")
|
|
82
|
+
.option("-p, --profile <name>", "profile name", "default")
|
|
83
|
+
.action(async (opts) => {
|
|
84
|
+
const ok = await deleteProfile(opts.profile);
|
|
85
|
+
process.stdout.write(ok ? chalk.green(`✓ Removed profile "${opts.profile}"\n`) : `(no profile "${opts.profile}")\n`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { call } from "../api.mjs";
|
|
3
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate } from "../ui.mjs";
|
|
4
|
+
|
|
5
|
+
function parseCsv(text) {
|
|
6
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
|
|
7
|
+
if (!lines.length) return [];
|
|
8
|
+
const split = (line) => {
|
|
9
|
+
const out = []; let cur = ""; let q = false;
|
|
10
|
+
for (let i = 0; i < line.length; i++) {
|
|
11
|
+
const c = line[i];
|
|
12
|
+
if (q) {
|
|
13
|
+
if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
|
|
14
|
+
else if (c === '"') { q = false; }
|
|
15
|
+
else cur += c;
|
|
16
|
+
} else {
|
|
17
|
+
if (c === ",") { out.push(cur); cur = ""; }
|
|
18
|
+
else if (c === '"') { q = true; }
|
|
19
|
+
else cur += c;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
out.push(cur);
|
|
23
|
+
return out.map((s) => s.trim());
|
|
24
|
+
};
|
|
25
|
+
const headers = split(lines[0]).map((h) => h.toLowerCase());
|
|
26
|
+
return lines.slice(1).map((line) => {
|
|
27
|
+
const cells = split(line);
|
|
28
|
+
const row = {};
|
|
29
|
+
headers.forEach((h, i) => { row[h] = cells[i] ?? ""; });
|
|
30
|
+
return row;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadRows(file) {
|
|
35
|
+
const text = await readFile(file, "utf8");
|
|
36
|
+
if (file.endsWith(".jsonl")) {
|
|
37
|
+
return text.split(/\r?\n/).filter(Boolean).map((l) => JSON.parse(l));
|
|
38
|
+
}
|
|
39
|
+
if (file.endsWith(".json")) {
|
|
40
|
+
const parsed = JSON.parse(text);
|
|
41
|
+
return Array.isArray(parsed) ? parsed : (parsed.leads ?? []);
|
|
42
|
+
}
|
|
43
|
+
return parseCsv(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function registerFlow(program) {
|
|
47
|
+
withCommonOpts(program.command("campaign-flow"))
|
|
48
|
+
.description("End-to-end: import leads → create/attach sequence → launch → show status")
|
|
49
|
+
.requiredOption("--name <name>", "campaign name")
|
|
50
|
+
.requiredOption("--file <path>", "leads CSV / JSON / JSONL")
|
|
51
|
+
.option("--description <text>", "campaign description")
|
|
52
|
+
.option("--goal <text>", "campaign goal")
|
|
53
|
+
.option("--persona <tag>", "default persona_tag for imported leads")
|
|
54
|
+
.option("--source <s>", "default lead source", "cli-flow")
|
|
55
|
+
.option("--sequence <id>", "attach an existing sequence (otherwise picks most recent active)")
|
|
56
|
+
.option("--senders <csv>", "comma-separated sender_account_ids")
|
|
57
|
+
.option("--daily-limit <n>", "daily send cap", "25")
|
|
58
|
+
.option("--launch", "activate the campaign immediately after setup", false)
|
|
59
|
+
.option("--dry-run", "print plan only, do not call the API", false)
|
|
60
|
+
.action(async function (opts) {
|
|
61
|
+
const ctx = ctxFrom(this);
|
|
62
|
+
const steps = [];
|
|
63
|
+
const log = (msg) => { if (!this.optsWithGlobals().json) process.stdout.write(msg); };
|
|
64
|
+
try {
|
|
65
|
+
// 1. Load + import leads
|
|
66
|
+
const rows = await loadRows(opts.file);
|
|
67
|
+
if (!rows.length) return fail(new Error(`No leads found in ${opts.file}`));
|
|
68
|
+
if (opts.persona) rows.forEach((r) => { r.persona_tag = r.persona_tag || opts.persona; });
|
|
69
|
+
rows.forEach((r) => { r.source = r.source || opts.source; });
|
|
70
|
+
|
|
71
|
+
if (opts.dryRun) {
|
|
72
|
+
log(`DRY RUN — would import ${rows.length} leads and create campaign "${opts.name}"\n`);
|
|
73
|
+
return emit(this, { dry_run: true, lead_count: rows.length, name: opts.name }, () => {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
log(`→ Importing ${rows.length} leads…\n`);
|
|
77
|
+
let imported = 0;
|
|
78
|
+
const importedIds = [];
|
|
79
|
+
for (let i = 0; i < rows.length; i += 200) {
|
|
80
|
+
const chunk = rows.slice(i, i + 200);
|
|
81
|
+
const r = await call("leads", "bulk_upsert", { leads: chunk }, ctx);
|
|
82
|
+
imported += r.count ?? 0;
|
|
83
|
+
(r.leads || r.upserted || []).forEach((l) => l?.id && importedIds.push(l.id));
|
|
84
|
+
}
|
|
85
|
+
steps.push({ step: "import_leads", imported, total: rows.length });
|
|
86
|
+
log(` ✓ Upserted ${imported}/${rows.length} leads\n`);
|
|
87
|
+
|
|
88
|
+
// Fallback: re-fetch ids if backend didn't return them
|
|
89
|
+
let leadIds = importedIds;
|
|
90
|
+
if (!leadIds.length) {
|
|
91
|
+
const list = await call("leads", "list", {
|
|
92
|
+
persona_tag: opts.persona, limit: Math.min(rows.length, 500),
|
|
93
|
+
}, ctx);
|
|
94
|
+
leadIds = (list.leads || []).map((l) => l.id).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Resolve sequence
|
|
98
|
+
let sequenceId = opts.sequence;
|
|
99
|
+
if (!sequenceId) {
|
|
100
|
+
const seqs = await call("sequences", "list", { limit: 50 }, ctx);
|
|
101
|
+
const active = (seqs.sequences || []).find((s) => s.status === "active")
|
|
102
|
+
|| (seqs.sequences || [])[0];
|
|
103
|
+
if (!active) return fail(new Error("No sequence found — pass --sequence <id> or create one first"));
|
|
104
|
+
sequenceId = active.id;
|
|
105
|
+
log(`→ Using sequence: ${active.name} (${truncate(sequenceId, 8)})\n`);
|
|
106
|
+
}
|
|
107
|
+
steps.push({ step: "resolve_sequence", sequence_id: sequenceId });
|
|
108
|
+
|
|
109
|
+
// 3. Create campaign
|
|
110
|
+
log(`→ Creating campaign "${opts.name}"…\n`);
|
|
111
|
+
const created = await call("campaigns", "create", {
|
|
112
|
+
name: opts.name,
|
|
113
|
+
description: opts.description,
|
|
114
|
+
goal: opts.goal,
|
|
115
|
+
sequence_id: sequenceId,
|
|
116
|
+
sender_accounts: opts.senders ? opts.senders.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
117
|
+
daily_send_limit: Number(opts.dailyLimit),
|
|
118
|
+
}, ctx);
|
|
119
|
+
const campaignId = created.campaign?.id;
|
|
120
|
+
if (!campaignId) return fail(new Error("Campaign create returned no id"));
|
|
121
|
+
steps.push({ step: "create_campaign", campaign_id: campaignId });
|
|
122
|
+
log(` ✓ Campaign ${truncate(campaignId, 8)} created\n`);
|
|
123
|
+
|
|
124
|
+
// 4. Attach leads
|
|
125
|
+
if (leadIds.length) {
|
|
126
|
+
log(`→ Attaching ${leadIds.length} leads to campaign…\n`);
|
|
127
|
+
const add = await call("campaigns", "add_leads", {
|
|
128
|
+
campaign_id: campaignId, lead_ids: leadIds,
|
|
129
|
+
}, ctx);
|
|
130
|
+
steps.push({ step: "add_leads", added: add.added, requested: add.requested });
|
|
131
|
+
log(` ✓ Added ${add.added}/${add.requested} (${add.valid} valid in workspace)\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. Launch (optional)
|
|
135
|
+
let launched = null;
|
|
136
|
+
if (opts.launch) {
|
|
137
|
+
log(`→ Launching campaign…\n`);
|
|
138
|
+
launched = await call("campaigns", "launch", { campaign_id: campaignId }, ctx);
|
|
139
|
+
steps.push({ step: "launch", status: launched.campaign?.status });
|
|
140
|
+
log(` ✓ Status → ${launched.campaign?.status}\n`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 6. Final status
|
|
144
|
+
const finalShow = await call("campaigns", "get", { id: campaignId }, ctx);
|
|
145
|
+
const c = finalShow.campaign || {};
|
|
146
|
+
const summary = {
|
|
147
|
+
campaign_id: campaignId,
|
|
148
|
+
name: c.name,
|
|
149
|
+
status: c.status,
|
|
150
|
+
sequence_id: c.sequence_id,
|
|
151
|
+
daily_send_limit: c.daily_send_limit,
|
|
152
|
+
sender_accounts: c.sender_accounts || [],
|
|
153
|
+
leads_attached: c.lead_count ?? leadIds.length,
|
|
154
|
+
steps,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
emit(this, summary, (d) => {
|
|
158
|
+
process.stdout.write("\n" + table(
|
|
159
|
+
["field", "value"],
|
|
160
|
+
[
|
|
161
|
+
["campaign", `${d.name} (${truncate(d.campaign_id, 8)})`],
|
|
162
|
+
["status", d.status],
|
|
163
|
+
["sequence", truncate(d.sequence_id, 8)],
|
|
164
|
+
["leads", d.leads_attached],
|
|
165
|
+
["senders", (d.sender_accounts || []).length],
|
|
166
|
+
["daily cap", d.daily_send_limit],
|
|
167
|
+
],
|
|
168
|
+
) + "\n");
|
|
169
|
+
process.stdout.write(opts.launch
|
|
170
|
+
? `✓ Campaign live — monitor with: advisoros campaigns show ${campaignId}\n`
|
|
171
|
+
: `✓ Campaign ready (draft) — launch with: advisoros campaigns launch ${campaignId}\n`);
|
|
172
|
+
});
|
|
173
|
+
} catch (e) { fail(e); }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { call } from "../api.mjs";
|
|
3
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate } from "../ui.mjs";
|
|
4
|
+
|
|
5
|
+
function parseCsv(text) {
|
|
6
|
+
// Tiny CSV parser: header row + comma-separated values, quoted fields supported.
|
|
7
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
|
|
8
|
+
if (!lines.length) return [];
|
|
9
|
+
const split = (line) => {
|
|
10
|
+
const out = []; let cur = ""; let q = false;
|
|
11
|
+
for (let i = 0; i < line.length; i++) {
|
|
12
|
+
const c = line[i];
|
|
13
|
+
if (q) {
|
|
14
|
+
if (c === '"' && line[i+1] === '"') { cur += '"'; i++; }
|
|
15
|
+
else if (c === '"') { q = false; }
|
|
16
|
+
else cur += c;
|
|
17
|
+
} else {
|
|
18
|
+
if (c === ",") { out.push(cur); cur = ""; }
|
|
19
|
+
else if (c === '"') { q = true; }
|
|
20
|
+
else cur += c;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
out.push(cur);
|
|
24
|
+
return out.map((s) => s.trim());
|
|
25
|
+
};
|
|
26
|
+
const headers = split(lines[0]).map((h) => h.toLowerCase());
|
|
27
|
+
return lines.slice(1).map((line) => {
|
|
28
|
+
const cells = split(line);
|
|
29
|
+
const row = {};
|
|
30
|
+
headers.forEach((h, i) => { row[h] = cells[i] ?? ""; });
|
|
31
|
+
return row;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerLeads(program) {
|
|
36
|
+
const leads = program.command("leads").description("Manage leads");
|
|
37
|
+
|
|
38
|
+
withCommonOpts(leads.command("list"))
|
|
39
|
+
.description("List leads")
|
|
40
|
+
.option("--status <s>", "filter by status")
|
|
41
|
+
.option("--persona <p>", "filter by persona_tag")
|
|
42
|
+
.option("--search <q>", "search name or company")
|
|
43
|
+
.option("--limit <n>", "max rows", "50")
|
|
44
|
+
.action(async function () {
|
|
45
|
+
try {
|
|
46
|
+
const o = this.optsWithGlobals();
|
|
47
|
+
const r = await call("leads", "list", {
|
|
48
|
+
status: o.status, persona_tag: o.persona, search: o.search, limit: Number(o.limit),
|
|
49
|
+
}, ctxFrom(this));
|
|
50
|
+
emit(this, r, (d) => {
|
|
51
|
+
process.stdout.write(table(
|
|
52
|
+
["id", "name", "title", "company", "status"],
|
|
53
|
+
(d.leads || []).map((l) => [
|
|
54
|
+
truncate(l.id, 8), truncate(l.name, 28), truncate(l.title, 24),
|
|
55
|
+
truncate(l.company, 22), l.status ?? "",
|
|
56
|
+
]),
|
|
57
|
+
) + `\n${d.leads?.length ?? 0} rows\n`);
|
|
58
|
+
});
|
|
59
|
+
} catch (e) { fail(e); }
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
withCommonOpts(leads.command("get <id>"))
|
|
63
|
+
.description("Show a single lead")
|
|
64
|
+
.action(async function (id) {
|
|
65
|
+
try {
|
|
66
|
+
const r = await call("leads", "get", { id }, ctxFrom(this));
|
|
67
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.lead, null, 2) + "\n"));
|
|
68
|
+
} catch (e) { fail(e); }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
withCommonOpts(leads.command("set-status <id> <status>"))
|
|
72
|
+
.description("Update a lead's status")
|
|
73
|
+
.action(async function (id, status) {
|
|
74
|
+
try {
|
|
75
|
+
const r = await call("leads", "update_status", { id, status }, ctxFrom(this));
|
|
76
|
+
emit(this, r, (d) => process.stdout.write(`✓ ${d.lead?.name ?? id} → ${d.lead?.status}\n`));
|
|
77
|
+
} catch (e) { fail(e); }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
withCommonOpts(leads.command("import"))
|
|
81
|
+
.description("Bulk upsert leads from a CSV or JSON/JSONL file")
|
|
82
|
+
.requiredOption("--file <path>", "leads.csv | leads.json | leads.jsonl")
|
|
83
|
+
.option("--persona <tag>", "default persona_tag for all rows")
|
|
84
|
+
.option("--source <s>", "default source", "cli")
|
|
85
|
+
.action(async function (opts) {
|
|
86
|
+
try {
|
|
87
|
+
const text = await readFile(opts.file, "utf8");
|
|
88
|
+
let rows;
|
|
89
|
+
if (opts.file.endsWith(".jsonl")) {
|
|
90
|
+
rows = text.split(/\r?\n/).filter(Boolean).map((l) => JSON.parse(l));
|
|
91
|
+
} else if (opts.file.endsWith(".json")) {
|
|
92
|
+
const parsed = JSON.parse(text);
|
|
93
|
+
rows = Array.isArray(parsed) ? parsed : (parsed.leads ?? []);
|
|
94
|
+
} else {
|
|
95
|
+
rows = parseCsv(text);
|
|
96
|
+
}
|
|
97
|
+
if (opts.persona) rows.forEach((r) => { r.persona_tag = r.persona_tag || opts.persona; });
|
|
98
|
+
rows.forEach((r) => { r.source = r.source || opts.source; });
|
|
99
|
+
|
|
100
|
+
// Send in chunks of 200 to respect API limit (500).
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for (let i = 0; i < rows.length; i += 200) chunks.push(rows.slice(i, i + 200));
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const chunk of chunks) {
|
|
105
|
+
const r = await call("leads", "bulk_upsert", { leads: chunk }, ctxFrom(this));
|
|
106
|
+
total += r.count ?? 0;
|
|
107
|
+
}
|
|
108
|
+
emit(this, { upserted: total, total_rows: rows.length }, (d) =>
|
|
109
|
+
process.stdout.write(`✓ Upserted ${d.upserted}/${d.total_rows} leads\n`));
|
|
110
|
+
} catch (e) { fail(e); }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
withCommonOpts(leads.command("verify <id>"))
|
|
114
|
+
.description("Run LinkedIn verification on a lead")
|
|
115
|
+
.action(async function (id) {
|
|
116
|
+
try {
|
|
117
|
+
const r = await call("leads", "verify", { id }, ctxFrom(this));
|
|
118
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.verify, null, 2) + "\n"));
|
|
119
|
+
} catch (e) { fail(e); }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function registerCampaigns(program) {
|
|
124
|
+
const c = program.command("campaigns").description("Manage LinkedIn campaigns");
|
|
125
|
+
|
|
126
|
+
withCommonOpts(c.command("list"))
|
|
127
|
+
.description("List campaigns")
|
|
128
|
+
.option("--status <s>", "filter by status")
|
|
129
|
+
.option("--limit <n>", "max rows", "50")
|
|
130
|
+
.action(async function () {
|
|
131
|
+
try {
|
|
132
|
+
const o = this.optsWithGlobals();
|
|
133
|
+
const r = await call("campaigns", "list", { status: o.status, limit: Number(o.limit) }, ctxFrom(this));
|
|
134
|
+
emit(this, r, (d) => {
|
|
135
|
+
process.stdout.write(table(
|
|
136
|
+
["id", "name", "status", "sequence", "daily", "senders"],
|
|
137
|
+
(d.campaigns || []).map((cp) => [
|
|
138
|
+
truncate(cp.id, 8), truncate(cp.name, 30), cp.status,
|
|
139
|
+
truncate(cp.sequence_id, 8), cp.daily_send_limit ?? "",
|
|
140
|
+
(cp.sender_accounts || []).length,
|
|
141
|
+
]),
|
|
142
|
+
) + `\n${d.campaigns?.length ?? 0} rows\n`);
|
|
143
|
+
});
|
|
144
|
+
} catch (e) { fail(e); }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
withCommonOpts(c.command("show <id>"))
|
|
148
|
+
.description("Show campaign details + stats")
|
|
149
|
+
.action(async function (id) {
|
|
150
|
+
try {
|
|
151
|
+
const r = await call("campaigns", "get", { id }, ctxFrom(this));
|
|
152
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.campaign, null, 2) + "\n"));
|
|
153
|
+
} catch (e) { fail(e); }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
withCommonOpts(c.command("create"))
|
|
157
|
+
.description("Create a new draft campaign")
|
|
158
|
+
.requiredOption("--name <name>", "campaign name")
|
|
159
|
+
.option("--description <text>", "description")
|
|
160
|
+
.option("--goal <text>", "campaign goal")
|
|
161
|
+
.option("--sequence <id>", "attach a sequence")
|
|
162
|
+
.option("--senders <csv>", "comma-separated sender_account_ids")
|
|
163
|
+
.option("--daily-limit <n>", "daily send cap", "25")
|
|
164
|
+
.action(async function (opts) {
|
|
165
|
+
try {
|
|
166
|
+
const r = await call("campaigns", "create", {
|
|
167
|
+
name: opts.name,
|
|
168
|
+
description: opts.description,
|
|
169
|
+
goal: opts.goal,
|
|
170
|
+
sequence_id: opts.sequence,
|
|
171
|
+
sender_accounts: opts.senders ? opts.senders.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
172
|
+
daily_send_limit: Number(opts.dailyLimit),
|
|
173
|
+
}, ctxFrom(this));
|
|
174
|
+
emit(this, r, (d) => process.stdout.write(`✓ Created campaign ${d.campaign?.id}\n`));
|
|
175
|
+
} catch (e) { fail(e); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
withCommonOpts(c.command("add-leads <campaignId>"))
|
|
179
|
+
.description("Attach leads to a campaign")
|
|
180
|
+
.option("--lead-ids <csv>", "comma-separated lead UUIDs")
|
|
181
|
+
.option("--file <path>", "file with one lead id per line")
|
|
182
|
+
.action(async function (campaignId, opts) {
|
|
183
|
+
try {
|
|
184
|
+
let ids = [];
|
|
185
|
+
if (opts.leadIds) ids = opts.leadIds.split(",").map((s) => s.trim()).filter(Boolean);
|
|
186
|
+
if (opts.file) {
|
|
187
|
+
const text = await readFile(opts.file, "utf8");
|
|
188
|
+
ids.push(...text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean));
|
|
189
|
+
}
|
|
190
|
+
if (!ids.length) return fail(new Error("--lead-ids or --file required"));
|
|
191
|
+
const r = await call("campaigns", "add_leads", { campaign_id: campaignId, lead_ids: ids }, ctxFrom(this));
|
|
192
|
+
emit(this, r, (d) => process.stdout.write(`✓ Added ${d.added}/${d.requested} leads (${d.valid} valid in workspace)\n`));
|
|
193
|
+
} catch (e) { fail(e); }
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
withCommonOpts(c.command("launch <id>"))
|
|
197
|
+
.description("Activate a campaign (status → active)")
|
|
198
|
+
.action(async function (id) {
|
|
199
|
+
try {
|
|
200
|
+
const r = await call("campaigns", "launch", { campaign_id: id }, ctxFrom(this));
|
|
201
|
+
emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
|
|
202
|
+
} catch (e) { fail(e); }
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
withCommonOpts(c.command("pause <id>"))
|
|
206
|
+
.description("Pause a campaign")
|
|
207
|
+
.action(async function (id) {
|
|
208
|
+
try {
|
|
209
|
+
const r = await call("campaigns", "pause", { campaign_id: id }, ctxFrom(this));
|
|
210
|
+
emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
|
|
211
|
+
} catch (e) { fail(e); }
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
withCommonOpts(c.command("resume <id>"))
|
|
215
|
+
.description("Resume a paused campaign")
|
|
216
|
+
.action(async function (id) {
|
|
217
|
+
try {
|
|
218
|
+
const r = await call("campaigns", "resume", { campaign_id: id }, ctxFrom(this));
|
|
219
|
+
emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
|
|
220
|
+
} catch (e) { fail(e); }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function registerSequences(program) {
|
|
225
|
+
const s = program.command("sequences").description("View LinkedIn sequences");
|
|
226
|
+
|
|
227
|
+
withCommonOpts(s.command("list"))
|
|
228
|
+
.description("List sequences")
|
|
229
|
+
.option("--limit <n>", "max rows", "50")
|
|
230
|
+
.action(async function () {
|
|
231
|
+
try {
|
|
232
|
+
const o = this.optsWithGlobals();
|
|
233
|
+
const r = await call("sequences", "list", { limit: Number(o.limit) }, ctxFrom(this));
|
|
234
|
+
emit(this, r, (d) => {
|
|
235
|
+
process.stdout.write(table(
|
|
236
|
+
["id", "name", "status"],
|
|
237
|
+
(d.sequences || []).map((s) => [truncate(s.id, 8), truncate(s.name, 40), s.status]),
|
|
238
|
+
) + `\n${d.sequences?.length ?? 0} rows\n`);
|
|
239
|
+
});
|
|
240
|
+
} catch (e) { fail(e); }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
withCommonOpts(s.command("show <id>"))
|
|
244
|
+
.description("Show sequence + steps")
|
|
245
|
+
.action(async function (id) {
|
|
246
|
+
try {
|
|
247
|
+
const r = await call("sequences", "get", { id }, ctxFrom(this));
|
|
248
|
+
emit(this, r, (d) => {
|
|
249
|
+
process.stdout.write(`Sequence: ${d.sequence?.name} (${d.sequence?.status})\n\n`);
|
|
250
|
+
process.stdout.write(table(
|
|
251
|
+
["#", "action", "channel", "delay", "variant", "preview"],
|
|
252
|
+
(d.steps || []).map((s) => [
|
|
253
|
+
s.step_order, s.action, s.channel, `${s.delay_days}d`, s.variant,
|
|
254
|
+
truncate(s.message_content, 60),
|
|
255
|
+
]),
|
|
256
|
+
) + "\n");
|
|
257
|
+
});
|
|
258
|
+
} catch (e) { fail(e); }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function registerInbox(program) {
|
|
263
|
+
const inbox = program.command("inbox").description("Inspect unified inbox");
|
|
264
|
+
|
|
265
|
+
withCommonOpts(inbox.command("threads"))
|
|
266
|
+
.description("List recent inbox messages")
|
|
267
|
+
.option("--campaign <id>", "filter by campaign_id")
|
|
268
|
+
.option("--lead <id>", "filter by lead_id")
|
|
269
|
+
.option("--needs-attention", "only messages flagged as needing attention")
|
|
270
|
+
.option("--limit <n>", "max rows", "50")
|
|
271
|
+
.action(async function () {
|
|
272
|
+
try {
|
|
273
|
+
const o = this.optsWithGlobals();
|
|
274
|
+
const r = await call("inbox", "list_threads", {
|
|
275
|
+
campaign_id: o.campaign, lead_id: o.lead,
|
|
276
|
+
needs_attention: o.needsAttention || undefined,
|
|
277
|
+
limit: Number(o.limit),
|
|
278
|
+
}, ctxFrom(this));
|
|
279
|
+
emit(this, r, (d) => {
|
|
280
|
+
process.stdout.write(table(
|
|
281
|
+
["created", "dir", "campaign", "body"],
|
|
282
|
+
(d.messages || []).map((m) => [
|
|
283
|
+
(m.created_at || "").slice(0, 16).replace("T", " "),
|
|
284
|
+
m.direction === "outbound" ? "→" : "←",
|
|
285
|
+
truncate(m.campaign_name || m.campaign_id, 20),
|
|
286
|
+
truncate((m.body || "").replace(/\s+/g, " "), 80),
|
|
287
|
+
]),
|
|
288
|
+
) + `\n${d.messages?.length ?? 0} rows\n`);
|
|
289
|
+
});
|
|
290
|
+
} catch (e) { fail(e); }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
withCommonOpts(inbox.command("send <leadId>"))
|
|
294
|
+
.description("Send a one-off LinkedIn DM (or first-touch invite note) to a lead")
|
|
295
|
+
.requiredOption("--text <message>", "message body (auto-truncated to channel limit)")
|
|
296
|
+
.option("--sender <accountId>", "sender_account_id (defaults to most-recently-updated linkedin_tool sender)")
|
|
297
|
+
.action(async function (leadId, opts) {
|
|
298
|
+
try {
|
|
299
|
+
const r = await call("inbox", "send_dm", {
|
|
300
|
+
lead_id: leadId,
|
|
301
|
+
text: opts.text,
|
|
302
|
+
sender_account_id: opts.sender,
|
|
303
|
+
}, ctxFrom(this));
|
|
304
|
+
emit(this, r, (d) => {
|
|
305
|
+
const s = d.sent || {};
|
|
306
|
+
const b = s.budget || {};
|
|
307
|
+
process.stdout.write(
|
|
308
|
+
`✓ ${s.channel_kind === "linkedin_invite" ? "Invite note" : "DM"} sent to ${s.lead_name || leadId}\n` +
|
|
309
|
+
` sender: ${s.sender_account_id}\n` +
|
|
310
|
+
` chars: ${s.chars}\n` +
|
|
311
|
+
` budget: invites ${b.invites_sent}/${b.invites_cap}, messages ${b.messages_sent}/${b.messages_cap}\n` +
|
|
312
|
+
(s.external_id ? ` msg id: ${s.external_id}\n` : ""),
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
} catch (e) { fail(e); }
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function registerContent(program) {
|
|
320
|
+
const c = program.command("content").description("Browse and publish content");
|
|
321
|
+
|
|
322
|
+
withCommonOpts(c.command("list"))
|
|
323
|
+
.description("List content items")
|
|
324
|
+
.option("--status <s>", "filter by status")
|
|
325
|
+
.option("--type <t>", "filter by type")
|
|
326
|
+
.option("--search <q>", "search title or body")
|
|
327
|
+
.option("--limit <n>", "max rows", "50")
|
|
328
|
+
.action(async function () {
|
|
329
|
+
try {
|
|
330
|
+
const o = this.optsWithGlobals();
|
|
331
|
+
const r = await call("content", "list", {
|
|
332
|
+
status: o.status, type: o.type, search: o.search, limit: Number(o.limit),
|
|
333
|
+
}, ctxFrom(this));
|
|
334
|
+
emit(this, r, (d) => {
|
|
335
|
+
process.stdout.write(table(
|
|
336
|
+
["id", "type", "status", "title"],
|
|
337
|
+
(d.content || []).map((c) => [truncate(c.id, 8), c.type, c.status, truncate(c.title, 60)]),
|
|
338
|
+
) + `\n${d.content?.length ?? 0} rows\n`);
|
|
339
|
+
});
|
|
340
|
+
} catch (e) { fail(e); }
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
withCommonOpts(c.command("show <id>"))
|
|
344
|
+
.description("Show a content item")
|
|
345
|
+
.action(async function (id) {
|
|
346
|
+
try {
|
|
347
|
+
const r = await call("content", "get", { id }, ctxFrom(this));
|
|
348
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.content, null, 2) + "\n"));
|
|
349
|
+
} catch (e) { fail(e); }
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
withCommonOpts(c.command("publish <id>"))
|
|
353
|
+
.description("Dispatch publication via agent-content-posting")
|
|
354
|
+
.action(async function (id) {
|
|
355
|
+
try {
|
|
356
|
+
const r = await call("content", "publish", { id }, ctxFrom(this));
|
|
357
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.publish, null, 2) + "\n"));
|
|
358
|
+
} catch (e) { fail(e); }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function registerAgents(program) {
|
|
363
|
+
const a = program.command("agents").description("List and invoke agents");
|
|
364
|
+
|
|
365
|
+
withCommonOpts(a.command("list"))
|
|
366
|
+
.description("List available agents")
|
|
367
|
+
.action(async function () {
|
|
368
|
+
try {
|
|
369
|
+
const r = await call("agents", "list", {}, ctxFrom(this));
|
|
370
|
+
emit(this, r, (d) => {
|
|
371
|
+
process.stdout.write(table(
|
|
372
|
+
["name", "status", "model"],
|
|
373
|
+
(d.agents || []).map((a) => [truncate(a.name, 32), a.status ?? "", truncate(a.model, 30)]),
|
|
374
|
+
) + `\n${d.agents?.length ?? 0} rows\n`);
|
|
375
|
+
});
|
|
376
|
+
} catch (e) { fail(e); }
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
withCommonOpts(a.command("run <name>"))
|
|
380
|
+
.description("Invoke an agent")
|
|
381
|
+
.option("--input <json>", "JSON input payload", "{}")
|
|
382
|
+
.action(async function (name, opts) {
|
|
383
|
+
try {
|
|
384
|
+
let input = {};
|
|
385
|
+
try { input = JSON.parse(opts.input); }
|
|
386
|
+
catch { return fail(new Error("--input must be valid JSON")); }
|
|
387
|
+
const r = await call("agents", "run", { agent_name: name, input }, ctxFrom(this));
|
|
388
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.run, null, 2) + "\n"));
|
|
389
|
+
} catch (e) { fail(e); }
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
withCommonOpts(a.command("job <id>"))
|
|
393
|
+
.description("Check agent_jobs status")
|
|
394
|
+
.action(async function (id) {
|
|
395
|
+
try {
|
|
396
|
+
const r = await call("agents", "job_status", { job_id: id }, ctxFrom(this));
|
|
397
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.job, null, 2) + "\n"));
|
|
398
|
+
} catch (e) { fail(e); }
|
|
399
|
+
});
|
|
400
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".advisoros");
|
|
6
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE_URL =
|
|
9
|
+
"https://iwcwydcxbzoenmqcqkcv.supabase.co/functions/v1/agent-api";
|
|
10
|
+
|
|
11
|
+
export const DEFAULTS = { baseUrl: DEFAULT_BASE_URL };
|
|
12
|
+
|
|
13
|
+
async function readConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
} catch {
|
|
18
|
+
return { profiles: {} };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function writeConfig(cfg) {
|
|
23
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true });
|
|
24
|
+
await writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf8");
|
|
25
|
+
try { await chmod(CONFIG_PATH, 0o600); } catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getProfile(name = "default") {
|
|
29
|
+
const cfg = await readConfig();
|
|
30
|
+
return cfg.profiles?.[name] ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function setProfile(name, data) {
|
|
34
|
+
const cfg = await readConfig();
|
|
35
|
+
cfg.profiles = cfg.profiles ?? {};
|
|
36
|
+
cfg.profiles[name] = data;
|
|
37
|
+
cfg.currentProfile = name;
|
|
38
|
+
await writeConfig(cfg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function listProfiles() {
|
|
42
|
+
const cfg = await readConfig();
|
|
43
|
+
return Object.keys(cfg.profiles ?? {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function deleteProfile(name) {
|
|
47
|
+
const cfg = await readConfig();
|
|
48
|
+
if (cfg.profiles?.[name]) {
|
|
49
|
+
delete cfg.profiles[name];
|
|
50
|
+
await writeConfig(cfg);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { CONFIG_PATH };
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { registerAuth } from "./commands/auth.mjs";
|
|
3
|
+
import {
|
|
4
|
+
registerLeads, registerCampaigns, registerSequences,
|
|
5
|
+
registerInbox, registerContent, registerAgents,
|
|
6
|
+
} from "./commands/resources.mjs";
|
|
7
|
+
import { registerFlow } from "./commands/flow.mjs";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name("advisoros")
|
|
13
|
+
.description("AdvisorOS CLI — drive leads, campaigns, sequences and agents from your terminal.")
|
|
14
|
+
.version("0.1.0")
|
|
15
|
+
.option("--json", "machine-readable JSON output (applies to all subcommands)");
|
|
16
|
+
|
|
17
|
+
registerAuth(program);
|
|
18
|
+
registerLeads(program);
|
|
19
|
+
registerCampaigns(program);
|
|
20
|
+
registerSequences(program);
|
|
21
|
+
registerInbox(program);
|
|
22
|
+
registerContent(program);
|
|
23
|
+
registerAgents(program);
|
|
24
|
+
registerFlow(program);
|
|
25
|
+
|
|
26
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
27
|
+
process.stderr.write(`✖ ${e?.message ?? e}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
|
|
4
|
+
export function isJsonMode(cmd) {
|
|
5
|
+
return Boolean(cmd?.optsWithGlobals?.().json ?? cmd?.opts?.().json);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function emit(cmd, data, renderHuman) {
|
|
9
|
+
if (isJsonMode(cmd)) {
|
|
10
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
renderHuman(data);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function fail(err) {
|
|
17
|
+
const msg = err?.message ?? String(err);
|
|
18
|
+
process.stderr.write(chalk.red(`✖ ${msg}\n`));
|
|
19
|
+
if (err?.body) {
|
|
20
|
+
try {
|
|
21
|
+
process.stderr.write(chalk.gray(JSON.stringify(err.body, null, 2)) + "\n");
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function table(headers, rows) {
|
|
28
|
+
const t = new Table({
|
|
29
|
+
head: headers.map((h) => chalk.bold.cyan(h)),
|
|
30
|
+
style: { head: [], border: ["gray"] },
|
|
31
|
+
wordWrap: true,
|
|
32
|
+
});
|
|
33
|
+
for (const row of rows) t.push(row);
|
|
34
|
+
return t.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function truncate(str, n = 40) {
|
|
38
|
+
if (str == null) return "";
|
|
39
|
+
const s = String(str);
|
|
40
|
+
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function withCommonOpts(cmd) {
|
|
44
|
+
return cmd
|
|
45
|
+
.option("--json", "output raw JSON")
|
|
46
|
+
.option("-w, --workspace <id>", "override workspace_id")
|
|
47
|
+
.option("-p, --profile <name>", "use a named profile")
|
|
48
|
+
.option("--api-key <key>", "override API key");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ctxFrom(cmd) {
|
|
52
|
+
const opts = cmd.optsWithGlobals();
|
|
53
|
+
return {
|
|
54
|
+
apiKey: opts.apiKey,
|
|
55
|
+
workspaceId: opts.workspace,
|
|
56
|
+
profile: opts.profile,
|
|
57
|
+
};
|
|
58
|
+
}
|