@wirecat/whatsapp-cli 0.1.0 → 0.2.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/README.md +213 -94
- package/dist/adapter/adapter.d.ts +8 -4
- package/dist/adapter/adapter.d.ts.map +1 -1
- package/dist/adapter/adapter.js +313 -85
- package/dist/adapter/adapter.js.map +1 -1
- package/dist/adapter/channels.d.ts +43 -0
- package/dist/adapter/channels.d.ts.map +1 -0
- package/dist/adapter/channels.js +100 -0
- package/dist/adapter/channels.js.map +1 -0
- package/dist/adapter/checks.d.ts +2 -3
- package/dist/adapter/checks.d.ts.map +1 -1
- package/dist/adapter/checks.js +1 -2
- package/dist/adapter/checks.js.map +1 -1
- package/dist/adapter/errors.d.ts +0 -5
- package/dist/adapter/errors.d.ts.map +1 -1
- package/dist/adapter/errors.js +7 -9
- package/dist/adapter/errors.js.map +1 -1
- package/dist/adapter/groups.d.ts +5 -1
- package/dist/adapter/groups.d.ts.map +1 -1
- package/dist/adapter/groups.js +51 -9
- package/dist/adapter/groups.js.map +1 -1
- package/dist/adapter/map.d.ts +16 -2
- package/dist/adapter/map.d.ts.map +1 -1
- package/dist/adapter/map.js +56 -7
- package/dist/adapter/map.js.map +1 -1
- package/dist/adapter/pins.d.ts +11 -0
- package/dist/adapter/pins.d.ts.map +1 -0
- package/dist/adapter/pins.js +10 -0
- package/dist/adapter/pins.js.map +1 -0
- package/dist/adapter/polls.d.ts +1 -1
- package/dist/adapter/polls.d.ts.map +1 -1
- package/dist/adapter/polls.js +2 -2
- package/dist/adapter/polls.js.map +1 -1
- package/dist/adapter/privacy.d.ts +18 -0
- package/dist/adapter/privacy.d.ts.map +1 -0
- package/dist/adapter/privacy.js +94 -0
- package/dist/adapter/privacy.js.map +1 -0
- package/dist/adapter/settings.d.ts +21 -0
- package/dist/adapter/settings.d.ts.map +1 -0
- package/dist/adapter/settings.js +76 -0
- package/dist/adapter/settings.js.map +1 -0
- package/dist/bin/wa.js +0 -13
- package/dist/bin/wa.js.map +1 -1
- package/dist/commands/session.d.ts +18 -7
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +92 -93
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +77 -129
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +3 -22
- package/dist/commands/update.js.map +1 -1
- package/dist/engine/channels.d.ts +100 -0
- package/dist/engine/channels.d.ts.map +1 -0
- package/dist/engine/channels.js +29 -0
- package/dist/engine/channels.js.map +1 -0
- package/dist/engine/contract.d.ts +475 -43
- package/dist/engine/contract.d.ts.map +1 -1
- package/dist/engine/contract.js +178 -11
- package/dist/engine/contract.js.map +1 -1
- package/dist/engine/go-engine.d.ts.map +1 -1
- package/dist/engine/go-engine.js +23 -34
- package/dist/engine/go-engine.js.map +1 -1
- package/dist/engine/groups.d.ts +26 -14
- package/dist/engine/groups.d.ts.map +1 -1
- package/dist/engine/groups.js +2 -0
- package/dist/engine/groups.js.map +1 -1
- package/dist/engine/privacy.d.ts +33 -0
- package/dist/engine/privacy.d.ts.map +1 -0
- package/dist/engine/privacy.js +23 -0
- package/dist/engine/privacy.js.map +1 -0
- package/dist/engine/settings.d.ts +45 -0
- package/dist/engine/settings.d.ts.map +1 -0
- package/dist/engine/settings.js +34 -0
- package/dist/engine/settings.js.map +1 -0
- package/dist/install/postinstall.d.ts.map +1 -1
- package/dist/install/postinstall.js +4 -3
- package/dist/install/postinstall.js.map +1 -1
- package/dist/messenger.d.ts +3 -4
- package/dist/messenger.d.ts.map +1 -1
- package/dist/messenger.js +29 -14
- package/dist/messenger.js.map +1 -1
- package/dist/program.d.ts +5 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +10 -38
- package/dist/program.js.map +1 -1
- package/dist/reasons.d.ts.map +1 -1
- package/dist/reasons.js +8 -0
- package/dist/reasons.js.map +1 -1
- package/dist/update.d.ts +0 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +0 -7
- package/dist/update.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -10
- package/skills/whatsapp-cli/SKILL.md +59 -24
- package/dist/commands/answer.d.ts +0 -7
- package/dist/commands/answer.d.ts.map +0 -1
- package/dist/commands/answer.js +0 -11
- package/dist/commands/answer.js.map +0 -1
package/dist/commands/session.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
2
|
import { CliError, indent, singleLine } from "@wirecat/cli-base";
|
|
4
3
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
5
|
-
import {
|
|
6
|
-
import { asFirstWord, commandWords, environmentOf,
|
|
4
|
+
import { atTerminal, loginQr, readSecret } from "@wirecat/cli-core";
|
|
5
|
+
import { asFirstWord, commandWords, environmentOf, logsInOrOut, messengerContext, printItem, REMOTE_WRITE, refuseCommandName, rootOf, secretInputOf, } from "@wirecat/cli-core/cli";
|
|
7
6
|
import { Argument, Command } from "commander";
|
|
8
7
|
import { linkDevice, WHATSAPP } from "../messenger.js";
|
|
9
8
|
import { privateStore } from "../paths.js";
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Shown once, before the code that links the device. It ignores `--quiet` the way the QR code does: it belongs to
|
|
11
|
+
* the prompt the person is answering, and after the link it is too late to read it.
|
|
12
|
+
*/
|
|
13
|
+
export const BAN_WARNING = [
|
|
14
|
+
"Before you link: wa is not an official WhatsApp app, and WhatsApp may ban an account that uses one.",
|
|
15
|
+
"It also bans accounts that send in bulk or write to people who do not know them.",
|
|
16
|
+
"wa writes only when a command asks, sends at most 30 messages an hour by default, and holds every",
|
|
17
|
+
"write when WhatsApp says to slow down or restricts the account. Never send in bulk or to strangers.",
|
|
18
|
+
].join("\n");
|
|
11
19
|
/** The phone pushes its history right after linking, once; how long to keep receiving it. */
|
|
12
|
-
export const HISTORY_WAIT = { firstMs: 120_000, quietMs: 45_000 };
|
|
20
|
+
export const HISTORY_WAIT = { firstMs: 120_000, quietMs: 45_000, slowMs: 20_000 };
|
|
13
21
|
/**
|
|
14
|
-
* Saves what the phone pushes until it goes quiet
|
|
15
|
-
*
|
|
22
|
+
* Saves what the phone pushes until it goes quiet or `signal` aborts. Live messages that arrive meanwhile are saved
|
|
23
|
+
* too, through the same connection.
|
|
16
24
|
*/
|
|
17
|
-
const receiveHistory = async (adapter, wait, signal) => {
|
|
25
|
+
const receiveHistory = async (adapter, wait, onSlow, signal) => {
|
|
18
26
|
const received = { chats: 0, messages: 0, people: 0 };
|
|
19
27
|
const stop = new AbortController();
|
|
20
28
|
const end = () => stop.abort();
|
|
21
29
|
let timer = setTimeout(end, wait.firstMs);
|
|
30
|
+
const slow = wait.slowMs === undefined ? undefined : setTimeout(onSlow, wait.slowMs);
|
|
22
31
|
signal?.addEventListener("abort", end, { once: true });
|
|
23
32
|
try {
|
|
24
33
|
await Promise.all([
|
|
@@ -26,6 +35,7 @@ const receiveHistory = async (adapter, wait, signal) => {
|
|
|
26
35
|
received.chats += batch.chats?.length ?? 0;
|
|
27
36
|
received.messages += batch.messages?.length ?? 0;
|
|
28
37
|
received.people += batch.people?.length ?? 0;
|
|
38
|
+
clearTimeout(slow);
|
|
29
39
|
clearTimeout(timer);
|
|
30
40
|
timer = setTimeout(end, wait.quietMs);
|
|
31
41
|
}, stop.signal),
|
|
@@ -33,17 +43,12 @@ const receiveHistory = async (adapter, wait, signal) => {
|
|
|
33
43
|
]);
|
|
34
44
|
}
|
|
35
45
|
finally {
|
|
46
|
+
clearTimeout(slow);
|
|
36
47
|
clearTimeout(timer);
|
|
37
48
|
signal?.removeEventListener("abort", end);
|
|
38
49
|
}
|
|
39
50
|
return received;
|
|
40
51
|
};
|
|
41
|
-
// Linking and unlinking change the account without passing cli-core's send guard, so the lock is checked here.
|
|
42
|
-
const refuseUnderReadOnly = (context) => {
|
|
43
|
-
const lock = context.settings.readOnlyLock;
|
|
44
|
-
if (lock)
|
|
45
|
-
throw new CliError("permission_error", `this process is read-only (${lock}) — it cannot link or unlink the device`);
|
|
46
|
-
};
|
|
47
52
|
const linkedText = (profile, account, got) => {
|
|
48
53
|
const word = asFirstWord(profile);
|
|
49
54
|
const name = account.name === null ? "you" : singleLine(account.name);
|
|
@@ -57,130 +62,124 @@ const linkedText = (profile, account, got) => {
|
|
|
57
62
|
].join("\n");
|
|
58
63
|
};
|
|
59
64
|
/** Links the profile's device unless it is linked already, then keeps the history the phone pushes once. */
|
|
60
|
-
export const startSession = async (command, context, { method,
|
|
65
|
+
export const startSession = async (command, context, { method, command: typed = "wa session start", signal, indent: pad = 0, stopEndsWait = false }) => {
|
|
61
66
|
const input = context.stdin;
|
|
62
|
-
const terminal =
|
|
63
|
-
|
|
64
|
-
throw new CliError("validation_error", "--qr-file is for a QR login", { param: "--qr-file" });
|
|
65
|
-
const unanswerable = () => new CliError("validation_error", `${typed} shows a code to scan or type — run it in a terminal, or pass --qr-file`, {
|
|
67
|
+
const terminal = atTerminal(input);
|
|
68
|
+
const unanswerable = () => new CliError("validation_error", `${typed} asks questions — run it in a terminal`, {
|
|
66
69
|
reason: "input.required",
|
|
67
70
|
});
|
|
68
71
|
// A device that is linked already asks nothing, so only a profile without one is refused before the engine starts.
|
|
69
|
-
if (!terminal &&
|
|
72
|
+
if (!terminal && method === "phone" && !existsSync(privateStore(context.profile, context.env)))
|
|
70
73
|
throw unanswerable();
|
|
71
|
-
const
|
|
74
|
+
const qr = loginQr({
|
|
75
|
+
diagnostic: context.streams.diagnostic,
|
|
76
|
+
note: context.renderer.note,
|
|
77
|
+
indent: pad,
|
|
78
|
+
terminal,
|
|
79
|
+
json: context.format !== "pretty",
|
|
80
|
+
});
|
|
72
81
|
const environment = environmentOf(command);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
phone: async () => {
|
|
87
|
-
if (!terminal)
|
|
88
|
-
throw new CliError("validation_error", `${typed} phone needs a terminal to ask for the number`, {
|
|
89
|
-
reason: "input.required",
|
|
90
|
-
});
|
|
91
|
-
return readSecret(`${" ".repeat(pad)}phone number, international format: `, {
|
|
92
|
-
input,
|
|
93
|
-
echo: true,
|
|
94
|
-
...(signal === undefined ? {} : { signal }),
|
|
82
|
+
const linked = await linkDevice(command, context, {
|
|
83
|
+
method,
|
|
84
|
+
beforeLink: () => {
|
|
85
|
+
signal?.throwIfAborted();
|
|
86
|
+
if (!terminal && method === "phone")
|
|
87
|
+
throw unanswerable();
|
|
88
|
+
context.streams.diagnostic(indent(BAN_WARNING, pad));
|
|
89
|
+
},
|
|
90
|
+
phone: async () => {
|
|
91
|
+
if (!terminal)
|
|
92
|
+
throw new CliError("validation_error", `${typed} phone needs a terminal to ask for the number`, {
|
|
93
|
+
reason: "input.required",
|
|
95
94
|
});
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
context.renderer.note(`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
return session;
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
// The image links the account for as long as it is valid; it does not outlive the login.
|
|
130
|
-
if (qrPath)
|
|
131
|
-
rmSync(qrPath, { force: true });
|
|
95
|
+
const number = await readSecret(`${" ".repeat(pad)}phone number, international format: `, {
|
|
96
|
+
...secretInputOf(command),
|
|
97
|
+
echo: true,
|
|
98
|
+
...(signal === undefined ? {} : { signal }),
|
|
99
|
+
});
|
|
100
|
+
// WhatsApp reads the leading digits as the country code, so a number typed without one pairs someone else's.
|
|
101
|
+
if (!number.trim().startsWith("+"))
|
|
102
|
+
throw new CliError("validation_error", "write the phone number with + and the country code, such as +34", {
|
|
103
|
+
param: "phone",
|
|
104
|
+
reason: "input.invalid",
|
|
105
|
+
});
|
|
106
|
+
return number;
|
|
107
|
+
},
|
|
108
|
+
onCode: ({ login, code }) => {
|
|
109
|
+
if (login === "pair") {
|
|
110
|
+
context.renderer.note(`pairing code: ${code}\n` +
|
|
111
|
+
"On the phone: tap the WhatsApp notification, or Settings > Linked devices > Link a device >\n" +
|
|
112
|
+
"Link with phone number instead, and type the code. It expires in about two minutes.");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
qr.show(code, "scan in WhatsApp → Settings → Linked devices → Link a device");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
signal?.throwIfAborted();
|
|
119
|
+
const session = { profile: context.profile, account: linked.account, alreadyLinked: linked.alreadyLinked };
|
|
120
|
+
if (!linked.alreadyLinked) {
|
|
121
|
+
context.renderer.note("receiving history from the phone; keep WhatsApp open on it. Ctrl+C stops — `serve` picks up the rest.");
|
|
122
|
+
const stop = stopEndsWait ? environment.untilStopped?.() : environment.signal;
|
|
123
|
+
const ended = AbortSignal.any([signal, stop].filter((one) => one !== undefined));
|
|
124
|
+
session.received = await context.withMessenger((adapter) => receiveHistory(adapter, environment.historyWait ?? HISTORY_WAIT, () => context.renderer.note("still waiting for the phone's history — keep WhatsApp open on the phone; it can take a minute"), ended), { listen: true });
|
|
132
125
|
}
|
|
126
|
+
return session;
|
|
133
127
|
};
|
|
134
128
|
const end = async (command) => {
|
|
135
129
|
const context = messengerContext(command, WHATSAPP);
|
|
136
|
-
refuseUnderReadOnly(context);
|
|
137
|
-
// Run by serve for us, the logout would end serve's own connection and the keys would go from under it.
|
|
138
|
-
if (environmentOf(command).relayed)
|
|
139
|
-
throw new CliError("provider_unavailable", `serve holds this WhatsApp login — stop it first (\`wa ${asFirstWord(context.profile)}server stop\`, or Ctrl+C ` +
|
|
140
|
-
"where it runs), then end the session", { reason: "device.in-use", sent: false });
|
|
141
130
|
const store = privateStore(context.profile, context.env);
|
|
142
131
|
if (!existsSync(store)) {
|
|
143
132
|
printItem(context, { profile: context.profile, ended: false });
|
|
144
133
|
return;
|
|
145
134
|
}
|
|
135
|
+
let dropped = false;
|
|
146
136
|
try {
|
|
147
137
|
await context.withMessenger((adapter) => adapter.logout());
|
|
148
138
|
}
|
|
149
139
|
catch (error) {
|
|
140
|
+
if (error instanceof CliError && error.details.reason === "device.in-use")
|
|
141
|
+
throw new CliError("provider_unavailable", `${error.message} — stop it first (\`wa ${asFirstWord(context.profile)}server stop\` for serve, or Ctrl+C ` +
|
|
142
|
+
"where it runs), then end the session", { ...error.details, sent: false });
|
|
150
143
|
// The phone may have removed the device already; the keys here are then worth nothing, and still go.
|
|
151
144
|
if (!(error instanceof CliError && error.code === "authentication_error"))
|
|
152
145
|
throw error;
|
|
153
|
-
|
|
146
|
+
dropped = true;
|
|
154
147
|
}
|
|
155
148
|
for (const suffix of ["", "-wal", "-shm", ".lock"])
|
|
156
149
|
rmSync(`${store}${suffix}`, { force: true });
|
|
150
|
+
if (dropped)
|
|
151
|
+
context.renderer.note("WhatsApp had already dropped this device; deleted its keys here");
|
|
157
152
|
printItem(context, { profile: context.profile, ended: true });
|
|
158
153
|
};
|
|
154
|
+
export const AGENTS_LINK_BY_QR = "Agents: run `wa <profile> session start qr` in the background — it waits for the scan, and a shell tool\n" +
|
|
155
|
+
"shows nothing until it exits. Read its output and paste the QR text block into the chat as a code block,\n" +
|
|
156
|
+
"exactly as printed. WhatsApp renews the code after 60 seconds, then about every 20: paste each new block\n" +
|
|
157
|
+
'at once and say the old one expired. With --json each code is also a stderr line {"event":"qr","qr":{...}}.\n' +
|
|
158
|
+
"Linking is not offered over MCP; an agent with only MCP asks the person to run the command.\n";
|
|
159
159
|
export const sessionCommand = () => {
|
|
160
160
|
// Linking waits for a person, and unlinking removes the owner's device: neither is an agent's call.
|
|
161
161
|
const session = annotate(new Command("session"), { mcp: false }).description("link this profile to WhatsApp as a device, or unlink it");
|
|
162
|
-
annotate(session.command("start"), {
|
|
162
|
+
annotate(logsInOrOut(session.command("start")), {
|
|
163
163
|
effects: REMOTE_WRITE,
|
|
164
164
|
lifetime: "interactive",
|
|
165
165
|
examples: ["wa personal session start", "wa personal session start phone"],
|
|
166
166
|
})
|
|
167
167
|
.description("link by QR code (default) or a pairing code, then keep the pushed history")
|
|
168
168
|
.addArgument(new Argument("[method]", "how to link").choices(["qr", "phone"]).default("qr"))
|
|
169
|
-
.option("--qr-file <png>", "write the QR code to this PNG instead of drawing it, for an agent to pass on")
|
|
170
169
|
.addHelpText("after", "\nFirst time? Use `wa personal setup` for linking, a check of five chats and an agent skill.\n" +
|
|
171
170
|
"\nqr: scan the code in WhatsApp > Settings > Linked devices > Link a device.\n" +
|
|
172
|
-
"phone: type your number here, then type the code WhatsApp shows here into
|
|
171
|
+
"phone: type your number here with + and the country code, then type the code WhatsApp shows here into\n" +
|
|
172
|
+
"the phone.\n" +
|
|
173
173
|
"\nRight after linking, the phone pushes your history once; this command keeps it in the local store\n" +
|
|
174
174
|
"until the phone goes quiet. Then run `wa <profile> server install` to keep receiving.\n" +
|
|
175
|
-
|
|
175
|
+
`\n${AGENTS_LINK_BY_QR}`)
|
|
176
176
|
.action(async function (method) {
|
|
177
177
|
const context = messengerContext(this, WHATSAPP);
|
|
178
178
|
refuseCommandName(context.profile, commandWords(rootOf(this)), "wa");
|
|
179
|
-
const { qrFile } = this.opts();
|
|
180
179
|
const session = await startSession(this, context, {
|
|
181
180
|
command: `wa ${asFirstWord(context.profile)}session start`,
|
|
182
181
|
method,
|
|
183
|
-
|
|
182
|
+
stopEndsWait: true,
|
|
184
183
|
});
|
|
185
184
|
if (context.format === "pretty")
|
|
186
185
|
context.streams.data(linkedText(context.profile, session.account, session.received));
|
|
@@ -192,7 +191,7 @@ export const sessionCommand = () => {
|
|
|
192
191
|
...(session.received ? { received: session.received } : {}),
|
|
193
192
|
});
|
|
194
193
|
});
|
|
195
|
-
annotate(session.command("end"), { effects: REMOTE_WRITE, examples: ["wa personal session end"] })
|
|
194
|
+
annotate(logsInOrOut(session.command("end")), { effects: REMOTE_WRITE, examples: ["wa personal session end"] })
|
|
196
195
|
.description("unlink this device on WhatsApp's side and delete its keys here")
|
|
197
196
|
.action(async function () {
|
|
198
197
|
await end(this);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/commands/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/commands/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAA;AAe3C,eAAO,MAAM,YAAY,eA2JnB,CAAA"}
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,59 +2,21 @@ import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { CliError, indent, renderPretty, singleLine } from "@wirecat/cli-base";
|
|
3
3
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
4
4
|
import { installSkill } from "@wirecat/cli-base/skill";
|
|
5
|
-
import {
|
|
6
|
-
import { asFirstWord, commandWords, environmentOf, inputPolicy, messengerContext, REMOTE_WRITE, refuseCommandName, rootOf, runtime, withDeadline, } from "@wirecat/cli-core/cli";
|
|
5
|
+
import { AGENTS, agentFor, asFirstWord, commandWords, environmentOf, logsInOrOut, messengerContext, printItem, REMOTE_WRITE, refuseCommandName, rootOf, runtime, screenFor, secretInputOf, } from "@wirecat/cli-core/cli";
|
|
7
6
|
import { levelFor } from "@wirecat/cli-core/sends";
|
|
8
7
|
import { Command, Option } from "commander";
|
|
9
8
|
import { WA } from "../app.js";
|
|
10
9
|
import { SKILL, WHATSAPP } from "../messenger.js";
|
|
11
10
|
import { pathsFor, privateStore } from "../paths.js";
|
|
12
11
|
import { installer, PACKAGE } from "../update.js";
|
|
13
|
-
import { printItem } from "./answer.js";
|
|
14
12
|
import { startSession } from "./session.js";
|
|
15
|
-
const AGENTS = ["none", "codex", "cursor", "claude", "gemini", "all"];
|
|
16
13
|
const STEPS = 5;
|
|
17
14
|
const chatCount = (count) => `${count} ${count === 1 ? "chat" : "chats"}`;
|
|
18
|
-
const
|
|
19
|
-
/**
|
|
20
|
-
* A person sees each step as a heading with its details indented under it. `--quiet` hides both, as it
|
|
21
|
-
* hides notes; a machine mode keeps the plain notes it always had, since stderr there is a log.
|
|
22
|
-
*/
|
|
23
|
-
const screenFor = (context, quiet) => {
|
|
24
|
-
const person = context.format === "pretty";
|
|
25
|
-
const say = (text) => {
|
|
26
|
-
if (!quiet)
|
|
27
|
-
context.streams.diagnostic(text);
|
|
28
|
-
};
|
|
29
|
-
return {
|
|
30
|
-
step: (index, title) => person ? say(`\n[${index}/${STEPS}] ${title}`) : context.renderer.note(`${index}/${STEPS} — ${title}`),
|
|
31
|
-
detail: (text) => (person ? say(indent(text, DETAIL)) : context.renderer.note(text)),
|
|
32
|
-
title: (text) => (person ? say(text) : context.renderer.note(text)),
|
|
33
|
-
indent: person ? DETAIL : 0,
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
const agentFor = async (context, given, pad, signal) => {
|
|
37
|
-
if (given)
|
|
38
|
-
return given;
|
|
39
|
-
if (context.format !== "pretty" || !context.stdin.isTTY || inputPolicy(context.stdin).noInput)
|
|
40
|
-
return "none";
|
|
41
|
-
const answer = (await readSecret(`${" ".repeat(pad)}Agent [codex/cursor/claude/gemini/all/none] (none): `, {
|
|
42
|
-
input: context.stdin,
|
|
43
|
-
echo: true,
|
|
44
|
-
...(signal === undefined ? {} : { signal }),
|
|
45
|
-
}))
|
|
46
|
-
.trim()
|
|
47
|
-
.toLowerCase() || "none";
|
|
48
|
-
const agent = AGENTS.find((known) => known === answer);
|
|
49
|
-
if (agent === undefined)
|
|
50
|
-
throw new CliError("validation_error", `unknown agent — choose ${AGENTS.join(", ")} with --agent`);
|
|
51
|
-
return agent;
|
|
52
|
-
};
|
|
53
|
-
export const setupCommand = () => annotate(new Command("setup"), {
|
|
15
|
+
export const setupCommand = () => annotate(logsInOrOut(new Command("setup")), {
|
|
54
16
|
effects: REMOTE_WRITE,
|
|
55
17
|
lifetime: "interactive",
|
|
56
18
|
mcp: false,
|
|
57
|
-
examples: ["wa personal setup", "wa personal setup --agent codex", "wa personal setup --
|
|
19
|
+
examples: ["wa personal setup", "wa personal setup --agent codex", "wa personal setup --agent claude --json"],
|
|
58
20
|
})
|
|
59
21
|
.description("set up WhatsApp and connect your agent")
|
|
60
22
|
.configureHelp({ showGlobalOptions: true })
|
|
@@ -67,125 +29,111 @@ export const setupCommand = () => annotate(new Command("setup"), {
|
|
|
67
29
|
"\nAllow about 5 minutes: the phone pushes your recent history once, right after linking.\n" +
|
|
68
30
|
"Scan the QR in WhatsApp: Settings > Linked devices > Link a device.\n" +
|
|
69
31
|
"Type your phone number only in the terminal.\n" +
|
|
70
|
-
"\nAgents: read `wa skill show` first. Without a terminal,
|
|
71
|
-
" wa personal setup --
|
|
72
|
-
"
|
|
32
|
+
"\nAgents: read `wa skill show` first. Without a terminal, setup prints the QR code as text:\n" +
|
|
33
|
+
" wa personal setup --agent codex --json\n" +
|
|
34
|
+
"Run it in the background and paste each QR block into the chat, as for `session start qr`.\n" +
|
|
73
35
|
"Machine mode skips agent installation unless --agent is given.\n" +
|
|
74
36
|
"A device linked before is checked without linking again.\n" +
|
|
75
37
|
"\nSetup reads five chats from the local store. To keep receiving messages:\n" +
|
|
76
38
|
" wa personal server install\n")
|
|
77
39
|
.addOption(new Option("--agent <agent>", "install the skill for this agent; asks at a terminal, otherwise none").choices(AGENTS))
|
|
78
40
|
.addOption(new Option("--method <method>", "how to log in when there is no session").choices(["qr", "phone"]).default("qr"))
|
|
79
|
-
.option("--qr-file <png>", "write a temporary QR image for an agent to pass on; works without a terminal")
|
|
80
41
|
.action(async function () {
|
|
81
42
|
const context = messengerContext(this, WHATSAPP);
|
|
82
43
|
refuseCommandName(context.profile, commandWords(rootOf(this)), "wa");
|
|
83
44
|
const options = this.opts();
|
|
84
45
|
if (context.settings.offline)
|
|
85
46
|
throw new CliError("validation_error", "wa setup verifies WhatsApp — remove --offline");
|
|
86
|
-
if (options.qrFile && options.method !== "qr")
|
|
87
|
-
throw new CliError("validation_error", "--qr-file is for a QR login", { param: "--qr-file" });
|
|
88
47
|
for (const permission of ["account.show", "chats.list"]) {
|
|
89
48
|
if (levelFor(context.settings.permissions, permission).level === "deny")
|
|
90
49
|
throw new CliError("permission_error", `profile ${context.profile} denies ${permission} — setup cannot verify it`, { permission });
|
|
91
50
|
}
|
|
92
51
|
const devicePath = privateStore(context.profile, context.env);
|
|
93
|
-
if (!existsSync(devicePath) && !context.stdin.isTTY &&
|
|
94
|
-
throw new CliError("validation_error", "wa setup needs a terminal to
|
|
52
|
+
if (!existsSync(devicePath) && !context.stdin.isTTY && options.method === "phone")
|
|
53
|
+
throw new CliError("validation_error", "wa setup --method phone needs a terminal to ask for the number — run it locally, or link by QR", { reason: "input.required" });
|
|
54
|
+
const environment = environmentOf(this);
|
|
55
|
+
const { platform } = environment;
|
|
95
56
|
const prefix = `wa ${asFirstWord(context.profile)}`;
|
|
96
57
|
const command = installer(environmentOf(this).update) === "npx"
|
|
97
|
-
? `npm${
|
|
58
|
+
? `npm${platform === "win32" ? ".cmd" : ""} exec --yes --package=${PACKAGE} -- ${prefix}`
|
|
98
59
|
: prefix;
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
close: async () => {
|
|
103
|
-
cancellation.abort();
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
];
|
|
107
|
-
const track = (closeable) => {
|
|
108
|
-
closeables.push(closeable);
|
|
109
|
-
};
|
|
110
|
-
const screen = screenFor(context, this.optsWithGlobals().quiet === true);
|
|
60
|
+
// The command's own signal, not a phase's: Ctrl+C during the history wait ends the wait, not setup.
|
|
61
|
+
const signal = environment.commandSignal ?? new AbortController().signal;
|
|
62
|
+
const screen = screenFor(context, this.optsWithGlobals().quiet === true, STEPS);
|
|
111
63
|
const linkContext = { ...context, renderer: { ...context.renderer, note: screen.detail } };
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
cancellation.signal.throwIfAborted();
|
|
141
|
-
return adapter.me();
|
|
142
|
-
});
|
|
143
|
-
screen.detail(`✓ ${account.name === null ? "verified" : singleLine(account.name)}`);
|
|
144
|
-
screen.step(4, "A few chats");
|
|
145
|
-
// WhatsApp has no chat list to ask for: the store holds what the phone pushed and what serve received.
|
|
146
|
-
const chats = await context.withStore((store, key) => store.forAccount(key).chats.chats({ limit: 5, offset: 0 }));
|
|
147
|
-
screen.detail(`✓ ${chatCount(chats.items.length)} read from the local store`);
|
|
148
|
-
if (chats.items.length === 0)
|
|
149
|
-
screen.detail(`nothing kept yet — \`${command}server install\` starts receiving`);
|
|
150
|
-
screen.step(5, "Agent");
|
|
151
|
-
const agent = await agentFor(context, options.agent, screen.indent, cancellation.signal);
|
|
152
|
-
screen.detail(agent === "none" ? "✓ no agent skill installed" : `✓ ${agent}`);
|
|
153
|
-
const targets = agent === "all" ? ["claude", "agents"] : [agent === "claude" ? "claude" : "agents"];
|
|
154
|
-
cancellation.signal.throwIfAborted();
|
|
155
|
-
const written = agent === "none" ? [] : installSkill(WA, SKILL, { targets, env: context.env });
|
|
156
|
-
const next = {
|
|
157
|
-
instructions: `${command}skill show`,
|
|
158
|
-
inbox: `${command}inbox --limit 5`,
|
|
159
|
-
chats: `${command}chats list --limit 5`,
|
|
160
|
-
receive: `${command}server install`,
|
|
161
|
-
history_status: `${command}store status`,
|
|
162
|
-
...(agent === "none" ? { skill: `${command}skill install`, mcp: `${command}mcp config` } : {}),
|
|
163
|
-
};
|
|
164
|
-
return {
|
|
165
|
-
profile: context.profile,
|
|
166
|
-
runtime: runtime(),
|
|
167
|
-
paths,
|
|
168
|
-
device: { path: devicePath, reused },
|
|
169
|
-
account: { id: account.id, name: account.name },
|
|
170
|
-
...(history ? { history } : {}),
|
|
171
|
-
chats: { checked: chats.items.length, has_more: chats.hasMore },
|
|
172
|
-
agent: { name: agent, written },
|
|
173
|
-
next,
|
|
174
|
-
};
|
|
64
|
+
screen.title(`WhatsApp setup — profile ${context.profile}. Allow about 5 minutes; the phone sends recent history once.`);
|
|
65
|
+
screen.step(1, "This computer");
|
|
66
|
+
const paths = pathsFor(context.env);
|
|
67
|
+
for (const path of [paths.config, paths.state, paths.cache]) {
|
|
68
|
+
mkdirSync(path, { recursive: true });
|
|
69
|
+
accessSync(path, constants.W_OK);
|
|
70
|
+
}
|
|
71
|
+
screen.detail("✓ local directories ready");
|
|
72
|
+
if (platform === "win32")
|
|
73
|
+
screen.detail("Windows: use wa.cmd or npm.cmd if PowerShell blocks scripts. Open a new terminal after installing Node.js.");
|
|
74
|
+
screen.step(2, "Link this device");
|
|
75
|
+
const session = await startSession(this, linkContext, {
|
|
76
|
+
signal,
|
|
77
|
+
method: options.method,
|
|
78
|
+
command: `${prefix}setup`,
|
|
79
|
+
indent: screen.indent,
|
|
80
|
+
stopEndsWait: true,
|
|
81
|
+
});
|
|
82
|
+
environment.endUntilStopped?.();
|
|
83
|
+
const reused = session.alreadyLinked;
|
|
84
|
+
const history = session.received;
|
|
85
|
+
screen.detail(history
|
|
86
|
+
? `✓ linked; ${chatCount(history.chats)} and ${history.messages} messages kept from the phone`
|
|
87
|
+
: "✓ existing device, no new link");
|
|
88
|
+
screen.step(3, "Account");
|
|
89
|
+
const account = await context.withMessenger(async (adapter) => {
|
|
90
|
+
signal.throwIfAborted();
|
|
91
|
+
return adapter.me();
|
|
175
92
|
});
|
|
176
|
-
|
|
93
|
+
screen.detail(`✓ ${account.name === null ? "verified" : singleLine(account.name)}`);
|
|
94
|
+
screen.step(4, "A few chats");
|
|
95
|
+
// WhatsApp has no chat list to ask for: the store holds what the phone pushed and what serve received.
|
|
96
|
+
const chats = await context.withStore((store, key) => store.forAccount(key).chats.chats({ limit: 5, offset: 0 }));
|
|
97
|
+
screen.detail(`✓ ${chatCount(chats.items.length)} read from the local store`);
|
|
98
|
+
if (chats.items.length === 0)
|
|
99
|
+
screen.detail(`nothing kept yet — \`${command}server install\` starts receiving`);
|
|
100
|
+
screen.step(5, "Agent");
|
|
101
|
+
const agent = await agentFor(context, options.agent, screen.indent, { ...secretInputOf(this), signal });
|
|
102
|
+
screen.detail(agent === "none" ? "✓ no agent skill installed" : `✓ ${agent}`);
|
|
103
|
+
const targets = agent === "all" ? ["claude", "agents"] : [agent === "claude" ? "claude" : "agents"];
|
|
104
|
+
signal.throwIfAborted();
|
|
105
|
+
const written = agent === "none" ? [] : installSkill(WA, SKILL, { targets, env: context.env });
|
|
106
|
+
const next = {
|
|
107
|
+
instructions: `${command}skill show`,
|
|
108
|
+
inbox: `${command}inbox --limit 5`,
|
|
109
|
+
chats: `${command}chats list --limit 5`,
|
|
110
|
+
receive: `${command}server install`,
|
|
111
|
+
history_status: `${command}store status`,
|
|
112
|
+
...(agent === "none" ? { skill: `${command}skill install`, mcp: `${command}mcp config` } : {}),
|
|
113
|
+
};
|
|
114
|
+
const answer = {
|
|
115
|
+
profile: context.profile,
|
|
116
|
+
runtime: runtime(),
|
|
117
|
+
paths,
|
|
118
|
+
device: { path: devicePath, reused },
|
|
119
|
+
account: { id: account.id, name: account.name },
|
|
120
|
+
...(history ? { history } : {}),
|
|
121
|
+
chats: { checked: chats.items.length, has_more: chats.hasMore },
|
|
122
|
+
agent: { name: agent, written },
|
|
123
|
+
next,
|
|
124
|
+
};
|
|
177
125
|
if (context.format !== "pretty")
|
|
178
126
|
printItem(context, answer);
|
|
179
127
|
else {
|
|
180
128
|
const rows = {
|
|
181
129
|
"Try now": next.inbox,
|
|
182
130
|
"Keep receiving": `${next.receive} (a background server saves new messages)`,
|
|
183
|
-
"Agent skill": agent
|
|
131
|
+
"Agent skill": agent === "none"
|
|
184
132
|
? `skipped — install later: ${next.skill}`
|
|
185
|
-
: `installed for ${agent
|
|
133
|
+
: `installed for ${agent}; start a new agent session if it is not found`,
|
|
186
134
|
"For an agent": next.instructions,
|
|
187
135
|
};
|
|
188
|
-
context.streams.data(`\n✓ WhatsApp is ready — profile ${context.profile}, ${chatCount(chats.
|
|
136
|
+
context.streams.data(`\n✓ WhatsApp is ready — profile ${context.profile}, ${chatCount(chats.items.length)} checked\n\n` +
|
|
189
137
|
indent(renderPretty(rows, { color: context.color }), 2));
|
|
190
138
|
}
|
|
191
139
|
});
|