zcode-acp-server 0.50.0 → 0.51.1

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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/code-fingerprint.json +2 -2
  3. package/dist/config/account-provider.d.ts.map +1 -1
  4. package/dist/config/account-provider.js +12 -6
  5. package/dist/config/account-provider.js.map +1 -1
  6. package/dist/config/user-config.d.ts +39 -2
  7. package/dist/config/user-config.d.ts.map +1 -1
  8. package/dist/config/user-config.js +79 -2
  9. package/dist/config/user-config.js.map +1 -1
  10. package/dist/goal-loop/driver.d.ts.map +1 -1
  11. package/dist/goal-loop/driver.js +9 -0
  12. package/dist/goal-loop/driver.js.map +1 -1
  13. package/dist/handlers/background-tasks.d.ts +8 -0
  14. package/dist/handlers/background-tasks.d.ts.map +1 -1
  15. package/dist/handlers/background-tasks.js +30 -1
  16. package/dist/handlers/background-tasks.js.map +1 -1
  17. package/dist/handlers/server-requests.d.ts +26 -1
  18. package/dist/handlers/server-requests.d.ts.map +1 -1
  19. package/dist/handlers/server-requests.js +14 -1
  20. package/dist/handlers/server-requests.js.map +1 -1
  21. package/dist/handlers/session.d.ts +9 -0
  22. package/dist/handlers/session.d.ts.map +1 -1
  23. package/dist/handlers/session.js +52 -19
  24. package/dist/handlers/session.js.map +1 -1
  25. package/dist/push/config.d.ts +47 -0
  26. package/dist/push/config.d.ts.map +1 -0
  27. package/dist/push/config.js +82 -0
  28. package/dist/push/config.js.map +1 -0
  29. package/dist/push/push.d.ts +77 -0
  30. package/dist/push/push.d.ts.map +1 -0
  31. package/dist/push/push.js +139 -0
  32. package/dist/push/push.js.map +1 -0
  33. package/dist/push/wecom.d.ts +45 -0
  34. package/dist/push/wecom.d.ts.map +1 -0
  35. package/dist/push/wecom.js +91 -0
  36. package/dist/push/wecom.js.map +1 -0
  37. package/dist/remote/broadcast.d.ts +15 -0
  38. package/dist/remote/broadcast.d.ts.map +1 -1
  39. package/dist/remote/broadcast.js +22 -1
  40. package/dist/remote/broadcast.js.map +1 -1
  41. package/dist/remote/config.d.ts +2 -1
  42. package/dist/remote/config.d.ts.map +1 -1
  43. package/dist/remote/config.js +4 -3
  44. package/dist/remote/config.js.map +1 -1
  45. package/dist/remote/endpoint.d.ts.map +1 -1
  46. package/dist/remote/endpoint.js +4 -0
  47. package/dist/remote/endpoint.js.map +1 -1
  48. package/dist/remote/hub-server.d.ts.map +1 -1
  49. package/dist/remote/hub-server.js +46 -0
  50. package/dist/remote/hub-server.js.map +1 -1
  51. package/dist/remote/push-test-endpoint.d.ts +12 -0
  52. package/dist/remote/push-test-endpoint.d.ts.map +1 -0
  53. package/dist/remote/push-test-endpoint.js +67 -0
  54. package/dist/remote/push-test-endpoint.js.map +1 -0
  55. package/dist/server.d.ts +8 -0
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +13 -0
  58. package/dist/server.js.map +1 -1
  59. package/dist/settings/workflow.d.ts.map +1 -1
  60. package/dist/settings/workflow.js +7 -0
  61. package/dist/settings/workflow.js.map +1 -1
  62. package/dist/workflow/poller.d.ts +1 -0
  63. package/dist/workflow/poller.d.ts.map +1 -1
  64. package/dist/workflow/poller.js +20 -0
  65. package/dist/workflow/poller.js.map +1 -1
  66. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WeCom push configuration + the single shared ACTIVE predicate
3
+ * (push-backend-requirements §6/§8/§10).
4
+ *
5
+ * Precedence mirrors `mergeCommon` (file > env): the `push` section of
6
+ * ~/.config/zcode-acp/config.json wins over the ZCODE_ACP_PUSH_* env vars.
7
+ * ACTIVE = enabled AND complete credentials (corpId/agentId/secret) — a push
8
+ * that cannot summon anyone must not hold an offline interaction either, so
9
+ * `pushIfOffline` and the interaction hold read the SAME predicate. An
10
+ * OPTIONAL relay (static-IP proxy for the WeCom API) rides along when both
11
+ * url+token are present; a half-configured relay warns once and pushes direct.
12
+ *
13
+ * Resolved lazily ONCE per process: an enabled-but-incomplete setup logs one
14
+ * warning at first resolution and every hook then no-ops (silent degradation).
15
+ */
16
+ import { loadUserConfig } from "../config/user-config.js";
17
+ import { warn } from "../utils.js";
18
+ /** Same truthy set as `remoteEnabledLive` (remote/config.ts). */
19
+ const TRUTHY = ["1", "true", "yes", "on"];
20
+ export function resolvePushConfig(env) {
21
+ const file = loadUserConfig(env).push ?? {};
22
+ const enabled = file.enabled !== undefined
23
+ ? file.enabled
24
+ : TRUTHY.includes((env.ZCODE_ACP_PUSH_ENABLED ?? "").trim().toLowerCase());
25
+ if (!enabled)
26
+ return null;
27
+ const corpId = file.corpId ?? (env.ZCODE_ACP_PUSH_CORP_ID ?? "").trim();
28
+ const secret = file.secret ?? (env.ZCODE_ACP_PUSH_SECRET ?? "").trim();
29
+ const toUser = file.toUser ?? ((env.ZCODE_ACP_PUSH_TO_USER ?? "").trim() || "@all");
30
+ const agentId = file.agentId ?? Number.parseInt((env.ZCODE_ACP_PUSH_AGENT_ID ?? "").trim(), 10);
31
+ if (!corpId || !Number.isInteger(agentId) || agentId < 1 || !secret) {
32
+ warn("push: enabled but corpId/agentId/secret incomplete " +
33
+ "(config file or ZCODE_ACP_PUSH_* env) — push stays inactive");
34
+ return null;
35
+ }
36
+ const relayUrl = trimTrailingSlashes(file.relay?.url ?? env.ZCODE_ACP_PUSH_RELAY_URL ?? "");
37
+ const relayToken = file.relay?.token ?? (env.ZCODE_ACP_PUSH_RELAY_TOKEN ?? "").trim();
38
+ const notify = {
39
+ turn: file.notify?.turn !== false,
40
+ goal: file.notify?.goal !== false,
41
+ run: file.notify?.run !== false,
42
+ task: file.notify?.task !== false,
43
+ };
44
+ if (relayUrl && relayToken) {
45
+ return {
46
+ corpId,
47
+ agentId,
48
+ secret,
49
+ toUser,
50
+ contentDetail: file.contentDetail ?? "full",
51
+ relay: { url: relayUrl, token: relayToken },
52
+ notify,
53
+ };
54
+ }
55
+ if (relayUrl || relayToken) {
56
+ warn("push: relay needs BOTH url and token (config file push.relay or " +
57
+ "ZCODE_ACP_PUSH_RELAY_* env) — pushing direct from this host");
58
+ }
59
+ return { corpId, agentId, secret, toUser, contentDetail: file.contentDetail ?? "full", notify };
60
+ }
61
+ function trimTrailingSlashes(s) {
62
+ return s.trim().replace(/\/+$/, "");
63
+ }
64
+ /** undefined = not yet resolved; null = resolved inactive. */
65
+ let cached;
66
+ /** The shared ACTIVE predicate (resolve-once per process). */
67
+ export function pushActive() {
68
+ if (cached === undefined)
69
+ cached = resolvePushConfig(process.env);
70
+ return cached !== null;
71
+ }
72
+ /** The resolved config when ACTIVE, null otherwise (resolves on first use). */
73
+ export function pushConfig() {
74
+ if (cached === undefined)
75
+ cached = resolvePushConfig(process.env);
76
+ return cached ?? null;
77
+ }
78
+ /** Test hook: forget the process-wide resolution. */
79
+ export function resetPushConfigForTests() {
80
+ cached = undefined;
81
+ }
82
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/push/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Offline push dispatch (push-backend-requirements §5): one helper gates every
3
+ * event hook. Push fires only when the bridge has ZERO connected clients (an
4
+ * online client renders the event live) and push is ACTIVE — the single
5
+ * predicate from push/config.ts. Fire-and-forget: send failures warn and never
6
+ * reach the event path.
7
+ */
8
+ import type { ZcodeAcpServer } from "../server.js";
9
+ import { type WeComSender } from "./wecom.js";
10
+ export type PushKind = "permission" | "question" | "run" | "task" | "test" | "turn" | "goal";
11
+ /** Settled kinds routed through {@link pushSettled} (§5.2 — per-kind switches). */
12
+ export type PushSettledKind = Extract<PushKind, "turn" | "goal" | "run" | "task">;
13
+ /**
14
+ * "<project> / <session-title>" source label for settled pushes. It rides the
15
+ * leading bracket (never the body — `contentDetail: "minimal"` strips bodies),
16
+ * replacing the kind prefix whose information the title tail already carries.
17
+ * TOTAL (never throws): the push path is fire-and-forget by contract, and the
18
+ * server handle may be a partial object in tests.
19
+ */
20
+ export declare function pushSourceLabel(server: ZcodeAcpServer, acpSid?: string): string;
21
+ export interface PushEventData {
22
+ kind: PushKind;
23
+ title: string;
24
+ body?: string;
25
+ /**
26
+ * Source label (`<project> / <session>`, see {@link pushSourceLabel}). When
27
+ * present it REPLACES the kind in the leading bracket — `[kind] title`
28
+ * would duplicate what the title tail already says (e.g. "turn completed"),
29
+ * and the bracket is the one slot minimal mode never strips.
30
+ */
31
+ label?: string;
32
+ }
33
+ /** Test hook: forget the process-wide sender. */
34
+ export declare function resetPushSenderForTests(): void;
35
+ /** Test hook: inject a fake sender (null = inactive). */
36
+ export declare function setPushSenderForTests(s: WeComSender | null): void;
37
+ /**
38
+ * Render the contracted WeCom text: `[<kind or label>] <title>` + optional
39
+ * body line. A `label` replaces the kind in the leading bracket (see
40
+ * {@link PushEventData.label}). Under `contentDetail: "minimal"` the body
41
+ * carries no business strings (§4 — content transits Tencent) — the bracket
42
+ * prefix + title identify the event.
43
+ */
44
+ export declare function renderPushContent(cfg: {
45
+ contentDetail: "full" | "minimal";
46
+ }, data: PushEventData): string;
47
+ /** §5: the single gated dispatch helper — offline + ACTIVE or nothing. */
48
+ export declare function pushIfOffline(server: ZcodeAcpServer, data: PushEventData): void;
49
+ /**
50
+ * §5.2 settled-event dispatch (turn end / goal loop stop / workflow run /
51
+ * background task): NO client-presence gate — an online client renders the
52
+ * event live but cannot wake the user's phone, and the settled event itself
53
+ * IS the "come back" signal. Each kind is individually switchable via
54
+ * `push.notify.<kind>` (default on); the shared ACTIVE predicate still applies.
55
+ */
56
+ export declare function pushSettled(server: ZcodeAcpServer, data: PushEventData & {
57
+ kind: PushSettledKind;
58
+ }): void;
59
+ /**
60
+ * §5.1: derive the permission/question payload at the zero-clients branch of
61
+ * `requestAny` (broadcast.ts) — the single hook covering permission requests,
62
+ * AskUserQuestion, and elicitation.
63
+ */
64
+ export declare function pushInteractionIfOffline(server: ZcodeAcpServer, method: string, params?: unknown): void;
65
+ /**
66
+ * §7 test push — sends REGARDLESS of client presence (it verifies the channel,
67
+ * not an offline event). Structured result for the REST route.
68
+ */
69
+ export declare function sendTestPush(title: string): Promise<{
70
+ ok: true;
71
+ sent: number;
72
+ } | {
73
+ ok: false;
74
+ error: "push_disabled" | "send_failed";
75
+ message?: string;
76
+ }>;
77
+ //# sourceMappingURL=push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/push/push.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7F,mFAAmF;AACnF,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;AAElF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAI/E;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAaD,iDAAiD;AACjD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED,yDAAyD;AACzD,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IAAE,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,EAC1C,IAAI,EAAE,aAAa,GAClB,MAAM,CAIR;AAiBD,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAO/E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,aAAa,GAAG;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,GAC9C,IAAI,CAON;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,GACf,IAAI,CAiBN;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,GACZ,OAAO,CACN;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,eAAe,GAAG,aAAa,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAC1E,CAeA"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Offline push dispatch (push-backend-requirements §5): one helper gates every
3
+ * event hook. Push fires only when the bridge has ZERO connected clients (an
4
+ * online client renders the event live) and push is ACTIVE — the single
5
+ * predicate from push/config.ts. Fire-and-forget: send failures warn and never
6
+ * reach the event path.
7
+ */
8
+ import path from "node:path";
9
+ import { appendDiary } from "../crash-guards.js";
10
+ import { log, warn } from "../utils.js";
11
+ import { pushConfig } from "./config.js";
12
+ import { createWeComSender } from "./wecom.js";
13
+ /**
14
+ * "<project> / <session-title>" source label for settled pushes. It rides the
15
+ * leading bracket (never the body — `contentDetail: "minimal"` strips bodies),
16
+ * replacing the kind prefix whose information the title tail already carries.
17
+ * TOTAL (never throws): the push path is fire-and-forget by contract, and the
18
+ * server handle may be a partial object in tests.
19
+ */
20
+ export function pushSourceLabel(server, acpSid) {
21
+ const project = path.basename(server.projectCwd?.() ?? process.cwd());
22
+ const title = acpSid !== undefined ? server.sessionTitles?.get(acpSid) : undefined;
23
+ return title ? `${project} / ${title}` : project;
24
+ }
25
+ /** undefined = not yet built; null = push inactive. */
26
+ let sender;
27
+ function pushSender() {
28
+ if (sender === undefined) {
29
+ const cfg = pushConfig();
30
+ sender = cfg ? createWeComSender(cfg) : null;
31
+ }
32
+ return sender;
33
+ }
34
+ /** Test hook: forget the process-wide sender. */
35
+ export function resetPushSenderForTests() {
36
+ sender = undefined;
37
+ }
38
+ /** Test hook: inject a fake sender (null = inactive). */
39
+ export function setPushSenderForTests(s) {
40
+ sender = s;
41
+ }
42
+ /**
43
+ * Render the contracted WeCom text: `[<kind or label>] <title>` + optional
44
+ * body line. A `label` replaces the kind in the leading bracket (see
45
+ * {@link PushEventData.label}). Under `contentDetail: "minimal"` the body
46
+ * carries no business strings (§4 — content transits Tencent) — the bracket
47
+ * prefix + title identify the event.
48
+ */
49
+ export function renderPushContent(cfg, data) {
50
+ const body = cfg.contentDetail === "minimal" ? undefined : data.body;
51
+ const prefix = `[${data.label ?? data.kind}]`;
52
+ return body ? `${prefix} ${data.title}\n${body}` : `${prefix} ${data.title}`;
53
+ }
54
+ /** Fire-and-forget send tail shared by both dispatch helpers. */
55
+ function dispatchPush(cfg, s, data) {
56
+ const content = renderPushContent(cfg, data);
57
+ s.sendText(content).then(() => {
58
+ log(`push: ${data.kind} "${data.title}" sent via WeCom`);
59
+ // The success trail must outlive stderr (a closed window eats log()):
60
+ // "did the push leave this bridge?" is the first question a
61
+ // missed-notification report asks (observed 2026-09-29).
62
+ appendDiary(`push: ${data.kind} "${data.title}" delivered via WeCom`);
63
+ }, (e) => warn(`push: WeCom send failed: ${e instanceof Error ? e.message : String(e)}`));
64
+ }
65
+ /** §5: the single gated dispatch helper — offline + ACTIVE or nothing. */
66
+ export function pushIfOffline(server, data) {
67
+ if (server.clients.size > 0)
68
+ return;
69
+ const cfg = pushConfig();
70
+ if (!cfg)
71
+ return;
72
+ const s = pushSender();
73
+ if (!s)
74
+ return;
75
+ dispatchPush(cfg, s, data);
76
+ }
77
+ /**
78
+ * §5.2 settled-event dispatch (turn end / goal loop stop / workflow run /
79
+ * background task): NO client-presence gate — an online client renders the
80
+ * event live but cannot wake the user's phone, and the settled event itself
81
+ * IS the "come back" signal. Each kind is individually switchable via
82
+ * `push.notify.<kind>` (default on); the shared ACTIVE predicate still applies.
83
+ */
84
+ export function pushSettled(server, data) {
85
+ void server; // presence deliberately NOT checked (see above)
86
+ const cfg = pushConfig();
87
+ if (!cfg || !cfg.notify[data.kind])
88
+ return;
89
+ const s = pushSender();
90
+ if (!s)
91
+ return;
92
+ dispatchPush(cfg, s, data);
93
+ }
94
+ /**
95
+ * §5.1: derive the permission/question payload at the zero-clients branch of
96
+ * `requestAny` (broadcast.ts) — the single hook covering permission requests,
97
+ * AskUserQuestion, and elicitation.
98
+ */
99
+ export function pushInteractionIfOffline(server, method, params) {
100
+ const p = (params ?? {});
101
+ if (method === "session/request_permission") {
102
+ const detail = typeof p.toolCall?.title === "string" ? p.toolCall.title.trim() : "";
103
+ pushIfOffline(server, {
104
+ kind: "permission",
105
+ title: "Approval requested",
106
+ body: detail || undefined,
107
+ });
108
+ return;
109
+ }
110
+ const question = typeof p.message === "string" ? p.message.trim() : "";
111
+ pushIfOffline(server, {
112
+ kind: "question",
113
+ title: "Agent question",
114
+ body: question || undefined,
115
+ });
116
+ }
117
+ /**
118
+ * §7 test push — sends REGARDLESS of client presence (it verifies the channel,
119
+ * not an offline event). Structured result for the REST route.
120
+ */
121
+ export async function sendTestPush(title) {
122
+ const cfg = pushConfig();
123
+ const s = pushSender();
124
+ if (!cfg || !s)
125
+ return { ok: false, error: "push_disabled" };
126
+ try {
127
+ await s.sendText(renderPushContent(cfg, { kind: "test", title }));
128
+ appendDiary(`push: test "${title}" delivered via WeCom`);
129
+ return { ok: true, sent: 1 };
130
+ }
131
+ catch (e) {
132
+ return {
133
+ ok: false,
134
+ error: "send_failed",
135
+ message: e instanceof Error ? e.message : String(e),
136
+ };
137
+ }
138
+ }
139
+ //# sourceMappingURL=push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/push/push.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * WeCom (企业微信) self-built-app push client — the whole outbound surface is
3
+ * two HTTP endpoints (push-backend-requirements §9):
4
+ *
5
+ * GET /cgi-bin/gettoken?corpid=&corpsecret= → {access_token, expires_in}
6
+ * POST /cgi-bin/message/send?access_token= → {errcode: 0}
7
+ *
8
+ * Zero dependencies: platform `fetch`, injectable for tests. The access token
9
+ * is cached with a 5-minute expiry margin; errcode 40014/42001 (invalid /
10
+ * expired token) triggers ONE refresh + ONE send retry — a second failure is
11
+ * final for that push. Callers are fire-and-forget and only warn.
12
+ *
13
+ * With `relay` configured both calls go through that base URL instead (with an
14
+ * `x-relay-token` header) — a reverse proxy on a static-IP host, so WeCom's
15
+ * 企业可信IP check (errcode 60020) only ever sees that host's egress IP.
16
+ */
17
+ export interface WeComSender {
18
+ sendText(content: string): Promise<void>;
19
+ }
20
+ /** Minimal fetch surface — the platform fetch satisfies it; tests fake it. */
21
+ export interface WeComFetch {
22
+ (url: string, init?: {
23
+ method?: string;
24
+ headers?: Record<string, string>;
25
+ body?: string;
26
+ }): Promise<{
27
+ ok: boolean;
28
+ status: number;
29
+ json(): Promise<unknown>;
30
+ }>;
31
+ }
32
+ export declare function createWeComSender(cfg: {
33
+ corpId: string;
34
+ agentId: number;
35
+ secret: string;
36
+ toUser: string;
37
+ /** Route both API calls through a static-IP relay (see file header). */
38
+ relay?: {
39
+ url: string;
40
+ token: string;
41
+ };
42
+ }, fetchImpl?: WeComFetch): WeComSender;
43
+ /** Truncate to a byte cap without splitting a UTF-8 sequence. */
44
+ export declare function truncateUtf8(s: string, maxBytes: number): string;
45
+ //# sourceMappingURL=wecom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wecom.d.ts","sourceRoot":"","sources":["../../src/push/wecom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,CACE,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1E,OAAO,CAAC;QACT,EAAE,EAAE,OAAO,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;KAC1B,CAAC,CAAC;CACJ;AAUD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IACH,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC,EACD,SAAS,GAAE,UAAgC,GAC1C,WAAW,CA2Eb;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKhE"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * WeCom (企业微信) self-built-app push client — the whole outbound surface is
3
+ * two HTTP endpoints (push-backend-requirements §9):
4
+ *
5
+ * GET /cgi-bin/gettoken?corpid=&corpsecret= → {access_token, expires_in}
6
+ * POST /cgi-bin/message/send?access_token= → {errcode: 0}
7
+ *
8
+ * Zero dependencies: platform `fetch`, injectable for tests. The access token
9
+ * is cached with a 5-minute expiry margin; errcode 40014/42001 (invalid /
10
+ * expired token) triggers ONE refresh + ONE send retry — a second failure is
11
+ * final for that push. Callers are fire-and-forget and only warn.
12
+ *
13
+ * With `relay` configured both calls go through that base URL instead (with an
14
+ * `x-relay-token` header) — a reverse proxy on a static-IP host, so WeCom's
15
+ * 企业可信IP check (errcode 60020) only ever sees that host's egress IP.
16
+ */
17
+ const API_BASE = "https://qyapi.weixin.qq.com";
18
+ /** Refresh before expiry so a send never rides a token about to die. */
19
+ const TOKEN_MARGIN_MS = 5 * 60_000;
20
+ /** errcode 40014 = invalid token, 42001 = expired — both mean "refresh once". */
21
+ const INVALID_TOKEN_CODES = new Set([40014, 42001]);
22
+ /** WeCom text content cap — truncate from the end of the body. */
23
+ const MAX_CONTENT_BYTES = 2048;
24
+ export function createWeComSender(cfg, fetchImpl = fetch) {
25
+ const base = cfg.relay?.url ?? API_BASE;
26
+ const relayHeaders = cfg.relay ? { "x-relay-token": cfg.relay.token } : undefined;
27
+ let token = null;
28
+ async function readJson(resp) {
29
+ try {
30
+ return (await resp.json());
31
+ }
32
+ catch {
33
+ return { errcode: resp.status, errmsg: `HTTP ${resp.status} (non-JSON body)` };
34
+ }
35
+ }
36
+ async function fetchToken() {
37
+ const url = `${base}/cgi-bin/gettoken?corpid=${encodeURIComponent(cfg.corpId)}` +
38
+ `&corpsecret=${encodeURIComponent(cfg.secret)}`;
39
+ const body = await readJson(await fetchImpl(url, { headers: relayHeaders }));
40
+ if (body.errcode !== undefined && body.errcode !== 0) {
41
+ throw new Error(`wecom gettoken failed: ${body.errcode} ${body.errmsg ?? ""}`.trim());
42
+ }
43
+ if (!body.access_token)
44
+ throw new Error("wecom gettoken returned no access_token");
45
+ const ttl = typeof body.expires_in === "number" ? body.expires_in * 1000 : 7200_000;
46
+ token = {
47
+ value: body.access_token,
48
+ expiresAt: Date.now() + Math.max(ttl - TOKEN_MARGIN_MS, 60_000),
49
+ };
50
+ return body.access_token;
51
+ }
52
+ async function currentToken(force = false) {
53
+ if (!force && token && Date.now() < token.expiresAt)
54
+ return token.value;
55
+ return fetchToken();
56
+ }
57
+ async function postSend(t, content) {
58
+ const resp = await fetchImpl(`${base}/cgi-bin/message/send?access_token=${encodeURIComponent(t)}`, {
59
+ method: "POST",
60
+ headers: relayHeaders,
61
+ body: JSON.stringify({
62
+ touser: cfg.toUser,
63
+ msgtype: "text",
64
+ agentid: cfg.agentId,
65
+ text: { content },
66
+ }),
67
+ });
68
+ return readJson(resp);
69
+ }
70
+ async function sendText(content) {
71
+ const bounded = truncateUtf8(content, MAX_CONTENT_BYTES);
72
+ let resp = await postSend(await currentToken(), bounded);
73
+ if (resp.errcode !== undefined && INVALID_TOKEN_CODES.has(resp.errcode)) {
74
+ resp = await postSend(await currentToken(true), bounded);
75
+ }
76
+ if (resp.errcode !== undefined && resp.errcode !== 0) {
77
+ throw new Error(`wecom message/send failed: ${resp.errcode} ${resp.errmsg ?? ""}`.trim());
78
+ }
79
+ }
80
+ return { sendText };
81
+ }
82
+ /** Truncate to a byte cap without splitting a UTF-8 sequence. */
83
+ export function truncateUtf8(s, maxBytes) {
84
+ if (Buffer.byteLength(s, "utf8") <= maxBytes)
85
+ return s;
86
+ let chars = Math.floor(maxBytes / 4); // every char is at most 4 bytes
87
+ while (chars > 0 && Buffer.byteLength(s.slice(0, chars), "utf8") > maxBytes)
88
+ chars--;
89
+ return `${s.slice(0, chars)}…`;
90
+ }
91
+ //# sourceMappingURL=wecom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wecom.js","sourceRoot":"","sources":["../../src/push/wecom.ts"],"names":[],"mappings":"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"}
@@ -30,6 +30,13 @@ export interface ClientLike {
30
30
  * BEFORE `connect()` so the stdio connection is captured too.
31
31
  */
32
32
  export declare function trackConnections(app: acp.AgentApp, clients: ClientRegistry): void;
33
+ /**
34
+ * A request fired with zero connected clients. The offline-interaction hold
35
+ * (server-requests.ts §6) classifies on this type: with push ACTIVE the race
36
+ * stays pending instead of rejecting into the auto-decline path.
37
+ */
38
+ export declare class NoClientsError extends Error {
39
+ }
33
40
  /**
34
41
  * Registry of connected ACP clients (stdio editor + remote WebSocket clients).
35
42
  * Membership is managed by the entry point via the SDK's per-connection
@@ -43,6 +50,14 @@ export declare class ClientRegistry {
43
50
  add(cx: ClientLike): void;
44
51
  remove(cx: ClientLike): void;
45
52
  get size(): number;
53
+ /**
54
+ * Fired when a request finds ZERO connected clients (just before the
55
+ * NoClientsError throw): the offline-push hook (§5.1). Wired by the server
56
+ * at construction — remote-layer code must not import the push module
57
+ * directly (keeps the layer cycle-free). Fire-and-forget: observer throws
58
+ * are swallowed, the throw path always proceeds.
59
+ */
60
+ noClientsObserver?: (method: string, params?: unknown) => void;
46
61
  /**
47
62
  * Record a connection's `initialize` clientInfo name, keyed by the SDK's
48
63
  * per-connection root (same identity `notifyOthers` filters on) so payloads
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/remote/broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAIrD,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKjF;AAQD;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,KAAK,CAAiC;IAC9C,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEvD,GAAG,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IAIzB,MAAM,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IAI5B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKlD,uEAAuE;IACvE,MAAM,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI;IAOrC;;;;;OAKG;IACG,UAAU,CACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAC7D,OAAO,CAAC,IAAI,CAAC;IAUhB,yEAAyE;IACzE,SAAS,IAAI,GAAG,CAAC,YAAY;IAK7B;;;;;;;;;OASG;IACG,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9F,QAAQ,IAAI,UAAU,EAAE;CAGzB"}
1
+ {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/remote/broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAIrD,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKjF;AAQD;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,KAAK,CAAiC;IAC9C,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEvD,GAAG,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IAIzB,MAAM,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IAI5B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAE/D;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKlD,uEAAuE;IACvE,MAAM,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI;IAOrC;;;;;OAKG;IACG,UAAU,CACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAC7D,OAAO,CAAC,IAAI,CAAC;IAUhB,yEAAyE;IACzE,SAAS,IAAI,GAAG,CAAC,YAAY;IAK7B;;;;;;;;;OASG;IACG,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9F,QAAQ,IAAI,UAAU,EAAE;CAGzB"}
@@ -30,6 +30,13 @@ export function trackConnections(app, clients) {
30
30
  void conn.closed.then(() => clients.remove(conn.client));
31
31
  });
32
32
  }
33
+ /**
34
+ * A request fired with zero connected clients. The offline-interaction hold
35
+ * (server-requests.ts §6) classifies on this type: with push ACTIVE the race
36
+ * stays pending instead of rejecting into the auto-decline path.
37
+ */
38
+ export class NoClientsError extends Error {
39
+ }
33
40
  /**
34
41
  * Registry of connected ACP clients (stdio editor + remote WebSocket clients).
35
42
  * Membership is managed by the entry point via the SDK's per-connection
@@ -49,6 +56,14 @@ export class ClientRegistry {
49
56
  get size() {
50
57
  return this.clients.size;
51
58
  }
59
+ /**
60
+ * Fired when a request finds ZERO connected clients (just before the
61
+ * NoClientsError throw): the offline-push hook (§5.1). Wired by the server
62
+ * at construction — remote-layer code must not import the push module
63
+ * directly (keeps the layer cycle-free). Fire-and-forget: observer throws
64
+ * are swallowed, the throw path always proceeds.
65
+ */
66
+ noClientsObserver;
52
67
  /**
53
68
  * Record a connection's `initialize` clientInfo name, keyed by the SDK's
54
69
  * per-connection root (same identity `notifyOthers` filters on) so payloads
@@ -173,7 +188,13 @@ async function notifyAll(registry, method, params) {
173
188
  async function requestAny(registry, method, params, options) {
174
189
  const clients = registry.snapshot();
175
190
  if (clients.length === 0) {
176
- throw new Error(`broadcast: no connected clients (${method})`);
191
+ try {
192
+ registry.noClientsObserver?.(method, params);
193
+ }
194
+ catch {
195
+ // fire-and-forget hook; never block the throw
196
+ }
197
+ throw new NoClientsError(`broadcast: no connected clients (${method})`);
177
198
  }
178
199
  const controllers = clients.map(() => new AbortController());
179
200
  // Link a caller-provided signal: aborting it cancels EVERY inner request.
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../../src/remote/broadcast.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../../src/remote/broadcast.ts"],"names":[],"mappings":"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"}
@@ -28,7 +28,8 @@
28
28
  * tried in order before going headless)
29
29
  * ZCODE_ACP_HUB_TERMINAL_COMMAND=<sh> shell command template ({script})
30
30
  * ZCODE_ACP_WEB_DIR=<abs path> web-client dist for same-origin hosting
31
- * (file `remote.webDir` wins; empty = off)
31
+ * (file `remote.webDir` wins; empty = off;
32
+ * a leading ~ is expanded to the home dir)
32
33
  * 3. Built-in defaults.
33
34
  *
34
35
  * Process-role plumbing stays env-only by design (never file-configurable):
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG9E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC3B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAiC5C,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,GAAG,IAAI,CAmB3F;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,GAAG,IAAI,CAexF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,aAAa,CAyBvF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG9F,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC3B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAiC5C,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,GAAG,IAAI,CAmB3F;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,GAAG,IAAI,CAexF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,aAAa,CAyBvF"}
@@ -28,7 +28,8 @@
28
28
  * tried in order before going headless)
29
29
  * ZCODE_ACP_HUB_TERMINAL_COMMAND=<sh> shell command template ({script})
30
30
  * ZCODE_ACP_WEB_DIR=<abs path> web-client dist for same-origin hosting
31
- * (file `remote.webDir` wins; empty = off)
31
+ * (file `remote.webDir` wins; empty = off;
32
+ * a leading ~ is expanded to the home dir)
32
33
  * 3. Built-in defaults.
33
34
  *
34
35
  * Process-role plumbing stays env-only by design (never file-configurable):
@@ -36,7 +37,7 @@
36
37
  * ZCODE_ACP_REMOTE_PIN_CWD=1 pin session roots to the process cwd
37
38
  * ZCODE_ACP_RESUME_SESSION=<id> per-request boot-resume target (ADR-0017)
38
39
  */
39
- import { loadUserConfig } from "../config/user-config.js";
40
+ import { expandHomePath, loadUserConfig } from "../config/user-config.js";
40
41
  import { warn } from "../utils.js";
41
42
  export const DEFAULT_HUB_PORT = 8377;
42
43
  export const DEFAULT_BRIDGE_PORT = 8378;
@@ -60,7 +61,7 @@ function mergeCommon(env) {
60
61
  hubHost: file.hubHost ?? ((env.ZCODE_ACP_HUB_HOST ?? "").trim() || DEFAULT_HUB_HOST),
61
62
  bridgePort: file.bridgePort ??
62
63
  parsePort(env.ZCODE_ACP_REMOTE_PORT, DEFAULT_BRIDGE_PORT, "ZCODE_ACP_REMOTE_PORT"),
63
- webDir: file.webDir ?? (env.ZCODE_ACP_WEB_DIR ?? "").trim(),
64
+ webDir: file.webDir ?? expandHomePath((env.ZCODE_ACP_WEB_DIR ?? "").trim()),
64
65
  };
65
66
  }
66
67
  /** Parse remote config; null = disabled (or misconfigured → warned). */