openzca 0.1.16 → 0.1.18

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.
Files changed (3) hide show
  1. package/README.md +3 -8
  2. package/dist/cli.js +242 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,8 @@
1
1
  # openzca
2
2
 
3
- Free and open-source CLI for Zalo, built on [zca-js](https://github.com/nicenathapong/zca-js). Command structure compatible with [zca-cli.dev/docs](https://zca-cli.dev/docs).
3
+ Free and open-source CLI for Zalo, built on [zca-js](https://github.com/RFS-ADRENO/zca-js). Command structure compatible with [zca-cli.dev/docs](https://zca-cli.dev/docs).
4
4
 
5
- ## Integrate with OpenClaw ZaloUser plugin
6
-
7
- Prompt:
8
- ```
9
- help me to enable zalouser openclaw plugin. However, don't install zca cli follow their instruction, please use: npm install -g openzca@latest
10
- ```
5
+ ## Integrate with OpenClaw OpenZalo plugin (including legacy `zalouser`)
11
6
 
12
7
 
13
8
  ## Install
@@ -296,7 +291,7 @@ Supervised mode notes:
296
291
 
297
292
  ## Multi-account profiles
298
293
 
299
- Use `--profile <name>` or set `ZCA_PROFILE=<name>` to switch between accounts. Manage profiles with the `account` commands.
294
+ Use `--profile <name>` or set `OPENZCA_PROFILE=<name>` (or legacy `ZCA_PROFILE=<name>`) to switch between accounts. Manage profiles with the `account` commands.
300
295
 
301
296
  Profile data is stored in `~/.openzca/` (override with `OPENZCA_HOME`):
302
297
 
package/dist/cli.js CHANGED
@@ -164,7 +164,7 @@ async function removeProfile(name) {
164
164
  }
165
165
  async function resolveProfileName(flagProfile) {
166
166
  const db = await ensureProfilesDb();
167
- const picked = flagProfile && flagProfile.trim() || process.env.ZCA_PROFILE && process.env.ZCA_PROFILE.trim() || db.defaultProfile || DEFAULT_PROFILE;
167
+ const picked = flagProfile && flagProfile.trim() || (process.env.OPENZCA_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim()) || db.defaultProfile || DEFAULT_PROFILE;
168
168
  if (!db.profiles[picked]) {
169
169
  if (picked === DEFAULT_PROFILE) {
170
170
  await ensureProfile(DEFAULT_PROFILE);
@@ -765,7 +765,7 @@ async function currentProfile(_command) {
765
765
  async function profileForLogin() {
766
766
  const opts = program.opts();
767
767
  const explicit = opts.profile?.trim();
768
- const fromEnv = process.env.ZCA_PROFILE?.trim();
768
+ const fromEnv = process.env.OPENZCA_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim();
769
769
  if (explicit) {
770
770
  await ensureProfile(explicit);
771
771
  return explicit;
@@ -806,6 +806,139 @@ async function refreshCacheForProfile(profile, api) {
806
806
  groups: groups.length
807
807
  };
808
808
  }
809
+ function parsePositiveIntFromEnv(name, fallback) {
810
+ const raw = process.env[name]?.trim();
811
+ if (!raw) return fallback;
812
+ const parsed = Number.parseInt(raw, 10);
813
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
814
+ return parsed;
815
+ }
816
+ function isListenerAlreadyStarted(error) {
817
+ if (!(error instanceof Error)) return false;
818
+ return /already started/i.test(error.message);
819
+ }
820
+ function toErrorText(error) {
821
+ return error instanceof Error ? error.message : String(error);
822
+ }
823
+ async function withTimeout(task, timeoutMs, message) {
824
+ let timeoutId;
825
+ try {
826
+ const timeout = new Promise((_, reject) => {
827
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
828
+ });
829
+ return await Promise.race([task, timeout]);
830
+ } finally {
831
+ if (timeoutId) clearTimeout(timeoutId);
832
+ }
833
+ }
834
+ async function withUploadListener(api, command, task) {
835
+ const connectTimeoutMs = parsePositiveIntFromEnv(
836
+ "OPENZCA_UPLOAD_LISTENER_CONNECT_TIMEOUT_MS",
837
+ 8e3
838
+ );
839
+ const uploadTimeoutMs = parsePositiveIntFromEnv(
840
+ "OPENZCA_UPLOAD_TIMEOUT_MS",
841
+ 12e4
842
+ );
843
+ let startedHere = false;
844
+ const sinkError = (error) => {
845
+ writeDebugLine(
846
+ "msg.upload.listener.error",
847
+ {
848
+ message: toErrorText(error)
849
+ },
850
+ command
851
+ );
852
+ };
853
+ const sinkClosed = (code, reason) => {
854
+ writeDebugLine(
855
+ "msg.upload.listener.closed",
856
+ {
857
+ code,
858
+ reason: reason || void 0
859
+ },
860
+ command
861
+ );
862
+ };
863
+ api.listener.on("error", sinkError);
864
+ api.listener.on("closed", sinkClosed);
865
+ try {
866
+ await new Promise((resolve, reject) => {
867
+ let settled = false;
868
+ let timeoutId;
869
+ const cleanup = () => {
870
+ if (timeoutId) clearTimeout(timeoutId);
871
+ api.listener.off("connected", onConnected);
872
+ api.listener.off("error", onConnectError);
873
+ api.listener.off("closed", onConnectClosed);
874
+ };
875
+ const finish = (error) => {
876
+ if (settled) return;
877
+ settled = true;
878
+ cleanup();
879
+ if (error) {
880
+ reject(error);
881
+ return;
882
+ }
883
+ resolve();
884
+ };
885
+ const onConnected = () => {
886
+ writeDebugLine("msg.upload.listener.connected", void 0, command);
887
+ finish();
888
+ };
889
+ const onConnectError = (error) => {
890
+ finish(new Error(`Upload listener connection error: ${toErrorText(error)}`));
891
+ };
892
+ const onConnectClosed = (code, reason) => {
893
+ finish(
894
+ new Error(
895
+ `Upload listener closed before ready (code=${code}${reason ? `, reason=${reason}` : ""}).`
896
+ )
897
+ );
898
+ };
899
+ timeoutId = setTimeout(() => {
900
+ finish(new Error(`Timed out waiting ${connectTimeoutMs}ms for upload listener connection.`));
901
+ }, connectTimeoutMs);
902
+ api.listener.on("connected", onConnected);
903
+ api.listener.on("error", onConnectError);
904
+ api.listener.on("closed", onConnectClosed);
905
+ try {
906
+ api.listener.start();
907
+ startedHere = true;
908
+ writeDebugLine(
909
+ "msg.upload.listener.start",
910
+ {
911
+ connectTimeoutMs,
912
+ uploadTimeoutMs
913
+ },
914
+ command
915
+ );
916
+ } catch (error) {
917
+ if (isListenerAlreadyStarted(error)) {
918
+ writeDebugLine("msg.upload.listener.already_started", void 0, command);
919
+ finish();
920
+ return;
921
+ }
922
+ finish(error);
923
+ }
924
+ });
925
+ return await withTimeout(
926
+ task(),
927
+ uploadTimeoutMs,
928
+ `Timed out waiting ${uploadTimeoutMs}ms for file upload completion.`
929
+ );
930
+ } finally {
931
+ api.listener.off("error", sinkError);
932
+ api.listener.off("closed", sinkClosed);
933
+ if (startedHere) {
934
+ try {
935
+ api.listener.stop();
936
+ writeDebugLine("msg.upload.listener.stop", void 0, command);
937
+ } catch {
938
+ }
939
+ }
940
+ }
941
+ }
809
942
  async function fetchRecentMessagesViaListener(api, threadId, threadType, count) {
810
943
  return new Promise((resolve, reject) => {
811
944
  let settled = false;
@@ -1653,7 +1786,7 @@ program.hook("preAction", (_parent, actionCommand) => {
1653
1786
  argv: process.argv.slice(2),
1654
1787
  cwd: process.cwd(),
1655
1788
  profileFlag: getDebugOptions(actionCommand).profile ?? null,
1656
- envProfile: process.env.ZCA_PROFILE ?? null
1789
+ envProfile: process.env.OPENZCA_PROFILE ?? process.env.ZCA_PROFILE ?? null
1657
1790
  },
1658
1791
  actionCommand
1659
1792
  );
@@ -2082,6 +2215,32 @@ msg.command("undo <msgId> <cliMsgId> <threadId>").option("-g, --group", "Undo in
2082
2215
  }
2083
2216
  )
2084
2217
  );
2218
+ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group", "Edit in group").description("Edit message (compatibility shim: recall old message then resend new text)").action(
2219
+ wrapAction(
2220
+ async (msgId, cliMsgId, threadId, message, opts, command) => {
2221
+ const { api } = await requireApi(command);
2222
+ const type = asThreadType(opts.group);
2223
+ const undoResponse = await api.undo(
2224
+ {
2225
+ msgId,
2226
+ cliMsgId
2227
+ },
2228
+ threadId,
2229
+ type
2230
+ );
2231
+ const sendResponse = await api.sendMessage(message, threadId, type);
2232
+ output(
2233
+ {
2234
+ mode: "undo+send",
2235
+ nativeEditSupported: false,
2236
+ undo: undoResponse,
2237
+ send: sendResponse
2238
+ },
2239
+ false
2240
+ );
2241
+ }
2242
+ )
2243
+ );
2085
2244
  msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeatable)", collectValues, []).option("-g, --group", "Upload in group").description("Upload and send file(s)").action(
2086
2245
  wrapAction(
2087
2246
  async (arg1, arg2, opts, command) => {
@@ -2111,13 +2270,17 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
2111
2270
  );
2112
2271
  }
2113
2272
  await assertFilesExist(attachments);
2114
- const response = await api.sendMessage(
2115
- {
2116
- msg: "",
2117
- attachments
2118
- },
2119
- threadId,
2120
- asThreadType(opts.group)
2273
+ const response = await withUploadListener(
2274
+ api,
2275
+ command,
2276
+ async () => api.sendMessage(
2277
+ {
2278
+ msg: "",
2279
+ attachments
2280
+ },
2281
+ threadId,
2282
+ asThreadType(opts.group)
2283
+ )
2121
2284
  );
2122
2285
  output(response, false);
2123
2286
  } finally {
@@ -2164,6 +2327,75 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
2164
2327
  }
2165
2328
  )
2166
2329
  );
2330
+ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").description("Pin conversation").action(
2331
+ wrapAction(async (threadId, opts, command) => {
2332
+ const { api } = await requireApi(command);
2333
+ const type = asThreadType(opts.group);
2334
+ const response = await api.setPinnedConversations(true, threadId, type);
2335
+ output(
2336
+ {
2337
+ threadId,
2338
+ threadType: type === ThreadType.Group ? "group" : "user",
2339
+ pinned: true,
2340
+ response
2341
+ },
2342
+ false
2343
+ );
2344
+ })
2345
+ );
2346
+ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation").description("Unpin conversation").action(
2347
+ wrapAction(async (threadId, opts, command) => {
2348
+ const { api } = await requireApi(command);
2349
+ const type = asThreadType(opts.group);
2350
+ const response = await api.setPinnedConversations(false, threadId, type);
2351
+ output(
2352
+ {
2353
+ threadId,
2354
+ threadType: type === ThreadType.Group ? "group" : "user",
2355
+ pinned: false,
2356
+ response
2357
+ },
2358
+ false
2359
+ );
2360
+ })
2361
+ );
2362
+ msg.command("list-pins").option("-j, --json", "JSON output").description("List pinned conversations").action(
2363
+ wrapAction(async (opts, command) => {
2364
+ const { api } = await requireApi(command);
2365
+ const response = await api.getPinConversations();
2366
+ if (opts.json) {
2367
+ output(response, true);
2368
+ return;
2369
+ }
2370
+ output(
2371
+ response.conversations.map((threadId) => ({
2372
+ threadId,
2373
+ pinned: true
2374
+ })),
2375
+ false
2376
+ );
2377
+ })
2378
+ );
2379
+ msg.command("member-info <userId>").option("-j, --json", "JSON output").description("Get member/user profile info").action(
2380
+ wrapAction(async (userId, opts, command) => {
2381
+ const { api } = await requireApi(command);
2382
+ const response = await api.getUserInfo(userId);
2383
+ if (opts.json) {
2384
+ output(response, true);
2385
+ return;
2386
+ }
2387
+ const profiles = response.changed_profiles ?? {};
2388
+ const matchedProfile = profiles[userId] ?? profiles[`${userId}_0`] ?? Object.values(profiles)[0] ?? null;
2389
+ output(
2390
+ {
2391
+ userId,
2392
+ found: Boolean(matchedProfile),
2393
+ profile: matchedProfile
2394
+ },
2395
+ false
2396
+ );
2397
+ })
2398
+ );
2167
2399
  var group = program.command("group").description("Group management");
2168
2400
  group.command("list").option("-j, --json", "JSON output").description("List groups").action(
2169
2401
  wrapAction(async (opts, command) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {