@scopebond/agent 0.5.0 → 0.5.2

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/dist/ipc.js CHANGED
@@ -39,7 +39,7 @@ export async function startControl(dir, version, routes) {
39
39
  const token = randomBytes(24).toString("base64url");
40
40
  const expected = Buffer.from(token);
41
41
  const handle = async (req, res) => {
42
- const send = (status, body) => { res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" }); res.end(JSON.stringify(body)); };
42
+ const send = (status, body) => { res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }); res.end(JSON.stringify(body)); };
43
43
  const offered = Buffer.from(String(req.headers[TOKEN_HEADER] ?? ""));
44
44
  if (offered.length !== expected.length || !timingSafeEqual(offered, expected))
45
45
  return send(401, { error: "unauthorized" });
package/dist/ipc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"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","sourcesContent":["// The agent's local control channel: HTTP with a random token, over a named pipe on Windows (a random name) or a Unix\n// socket in a folder only this user can open, and for one more release also on 127.0.0.1 at a random port (the PowerShell\n// tray uses it; SCOPEBOND_AGENT_LOOPBACK=0 turns it off). The pipe or socket, the port, the token and the process id are\n// written to agent.json in the Scopebond home, which is how the CLI, the hook's override window and the tray find it.\n// A loopback port can be reached by any program on the computer; the pipe's name and the socket's folder cannot be found\n// or opened by another user. Every request must carry the token either way.\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { requestOverSocket } from \"@scopebond/hook\";\n\nexport const AGENT_FILE = \"agent.json\";\nexport const TOKEN_HEADER = \"x-scopebond-agent-token\";\n\n/** `socket`: the named pipe or Unix socket (absent from agents before it); `port`: 0 when loopback is off. */\nexport interface AgentEndpoint { port: number; socket?: string | null; token: string; pid: number; started_at: number; version: string }\n\n/** Where this start's pipe or socket goes. Windows: a pipe whose name is random, so no other user can guess it or claim it\n * first. Elsewhere: `run/agent.sock` in the Scopebond home (folder 0700), or a private folder under the temporary folder\n * when that path is longer than a Unix socket path may be. */\nexport function localSocketPath(dir: string, platform: NodeJS.Platform = process.platform, random = randomBytes(16).toString(\"hex\")): string {\n if (platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\scopebond-agent-${random}`;\n const inHome = join(dir, \"run\", \"agent.sock\");\n if (Buffer.byteLength(inHome) <= 100) return inHome;\n return join(mkdtempSync(join(tmpdir(), \"scopebond-\")), \"agent.sock\");\n}\n\nexport type Handler = (body: unknown) => Promise<unknown> | unknown;\n\nexport function readEndpoint(dir: string): AgentEndpoint | null {\n const file = join(dir, AGENT_FILE);\n if (!existsSync(file)) return null;\n try { return JSON.parse(readFileSync(file, \"utf8\")) as AgentEndpoint; } catch { return null; }\n}\n\n/** Start the control server. `routes` maps \"METHOD /path\" to a handler. */\nexport async function startControl(dir: string, version: string, routes: Record<string, Handler>): Promise<{ server: Server; endpoint: AgentEndpoint; close(): Promise<void> }> {\n const token = randomBytes(24).toString(\"base64url\");\n const expected = Buffer.from(token);\n const handle = async (req: IncomingMessage, res: ServerResponse) => {\n const send = (status: number, body: unknown) => { res.writeHead(status, { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" }); res.end(JSON.stringify(body)); };\n const offered = Buffer.from(String(req.headers[TOKEN_HEADER] ?? \"\"));\n if (offered.length !== expected.length || !timingSafeEqual(offered, expected)) return send(401, { error: \"unauthorized\" });\n const handler = routes[`${req.method} ${(req.url ?? \"/\").split(\"?\")[0]}`];\n if (!handler) return send(404, { error: \"not found\" });\n let raw = \"\";\n for await (const chunk of req) { raw += chunk; if (raw.length > 64 * 1024) return send(413, { error: \"too large\" }); }\n try { send(200, await handler(raw ? JSON.parse(raw) : null)); }\n catch (error) { send(500, { error: (error as Error).message.slice(0, 200) }); }\n };\n // The pipe or socket first. A Unix socket's folder is created for this user only, and the socket itself is too.\n const socketPath = localSocketPath(dir);\n const local = createServer(handle);\n let socket: string | null = socketPath;\n try {\n if (process.platform !== \"win32\") {\n mkdirSync(join(socketPath, \"..\"), { recursive: true, mode: 0o700 });\n chmodSync(join(socketPath, \"..\"), 0o700);\n rmSync(socketPath, { force: true });\n }\n await new Promise<void>((resolve, reject) => { local.once(\"error\", reject); local.listen(socketPath, () => resolve()); });\n if (process.platform !== \"win32\") chmodSync(socketPath, 0o600);\n } catch { socket = null; }\n // Loopback for one more release (the PowerShell tray), unless turned off; always when the pipe could not be made.\n const loopback = process.env.SCOPEBOND_AGENT_LOOPBACK !== \"0\" || socket === null;\n const server = createServer(handle);\n if (loopback) await new Promise<void>((resolve, reject) => { server.once(\"error\", reject); server.listen(0, \"127.0.0.1\", () => resolve()); });\n const address = loopback ? server.address() : null;\n const port = typeof address === \"object\" && address ? address.port : 0;\n const endpoint: AgentEndpoint = { port, socket, token, pid: process.pid, started_at: Date.now(), version };\n const file = join(dir, AGENT_FILE);\n const temp = `${file}.${process.pid}.tmp`;\n writeFileSync(temp, JSON.stringify(endpoint, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, file);\n return {\n server, endpoint,\n close: () => new Promise<void>((resolve) => {\n let open = (loopback ? 1 : 0) + (socket ? 1 : 0);\n const done = () => { if (--open <= 0) resolve(); };\n if (open === 0) resolve();\n if (loopback) server.close(done);\n if (socket) local.close(() => { if (process.platform !== \"win32\") rmSync(socketPath, { force: true }); done(); });\n // Only remove the file if it is still ours (a newer agent may have replaced it).\n try { if (readEndpoint(dir)?.pid === process.pid) rmSync(file, { force: true }); } catch { /* best effort */ }\n }),\n };\n}\n\n/** Call a running agent, over its pipe or socket when it has one. Returns null when no agent answers. */\nexport async function callAgent(dir: string, method: \"GET\" | \"POST\", path: string, body?: unknown, timeoutMs = 5_000): Promise<unknown | null> {\n const endpoint = readEndpoint(dir);\n if (!endpoint) return null;\n if (typeof endpoint.socket === \"string\" && endpoint.socket) {\n const answer = await requestOverSocket(endpoint.socket, method, path, { [TOKEN_HEADER]: endpoint.token }, body, timeoutMs);\n if (answer) return answer.status >= 200 && answer.status < 300 ? answer.body : null;\n if (!endpoint.port) return null;\n }\n try {\n const res = await fetch(`http://127.0.0.1:${endpoint.port}${path}`, {\n method, headers: { [TOKEN_HEADER]: endpoint.token, ...(body !== undefined ? { \"content-type\": \"application/json\" } : {}) },\n body: body !== undefined ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeoutMs),\n });\n return res.ok ? await res.json() : null;\n } catch { return null; }\n}\n"]}
1
+ {"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"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","sourcesContent":["// The agent's local control channel: HTTP with a random token, over a named pipe on Windows (a random name) or a Unix\n// socket in a folder only this user can open, and for one more release also on 127.0.0.1 at a random port (the PowerShell\n// tray uses it; SCOPEBOND_AGENT_LOOPBACK=0 turns it off). The pipe or socket, the port, the token and the process id are\n// written to agent.json in the Scopebond home, which is how the CLI, the hook's override window and the tray find it.\n// A loopback port can be reached by any program on the computer; the pipe's name and the socket's folder cannot be found\n// or opened by another user. Every request must carry the token either way.\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { requestOverSocket } from \"@scopebond/hook\";\n\nexport const AGENT_FILE = \"agent.json\";\nexport const TOKEN_HEADER = \"x-scopebond-agent-token\";\n\n/** `socket`: the named pipe or Unix socket (absent from agents before it); `port`: 0 when loopback is off. */\nexport interface AgentEndpoint { port: number; socket?: string | null; token: string; pid: number; started_at: number; version: string }\n\n/** Where this start's pipe or socket goes. Windows: a pipe whose name is random, so no other user can guess it or claim it\n * first. Elsewhere: `run/agent.sock` in the Scopebond home (folder 0700), or a private folder under the temporary folder\n * when that path is longer than a Unix socket path may be. */\nexport function localSocketPath(dir: string, platform: NodeJS.Platform = process.platform, random = randomBytes(16).toString(\"hex\")): string {\n if (platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\scopebond-agent-${random}`;\n const inHome = join(dir, \"run\", \"agent.sock\");\n if (Buffer.byteLength(inHome) <= 100) return inHome;\n return join(mkdtempSync(join(tmpdir(), \"scopebond-\")), \"agent.sock\");\n}\n\nexport type Handler = (body: unknown) => Promise<unknown> | unknown;\n\nexport function readEndpoint(dir: string): AgentEndpoint | null {\n const file = join(dir, AGENT_FILE);\n if (!existsSync(file)) return null;\n try { return JSON.parse(readFileSync(file, \"utf8\")) as AgentEndpoint; } catch { return null; }\n}\n\n/** Start the control server. `routes` maps \"METHOD /path\" to a handler. */\nexport async function startControl(dir: string, version: string, routes: Record<string, Handler>): Promise<{ server: Server; endpoint: AgentEndpoint; close(): Promise<void> }> {\n const token = randomBytes(24).toString(\"base64url\");\n const expected = Buffer.from(token);\n const handle = async (req: IncomingMessage, res: ServerResponse) => {\n const send = (status: number, body: unknown) => { res.writeHead(status, { \"content-type\": \"application/json; charset=utf-8\", \"cache-control\": \"no-store\" }); res.end(JSON.stringify(body)); };\n const offered = Buffer.from(String(req.headers[TOKEN_HEADER] ?? \"\"));\n if (offered.length !== expected.length || !timingSafeEqual(offered, expected)) return send(401, { error: \"unauthorized\" });\n const handler = routes[`${req.method} ${(req.url ?? \"/\").split(\"?\")[0]}`];\n if (!handler) return send(404, { error: \"not found\" });\n let raw = \"\";\n for await (const chunk of req) { raw += chunk; if (raw.length > 64 * 1024) return send(413, { error: \"too large\" }); }\n try { send(200, await handler(raw ? JSON.parse(raw) : null)); }\n catch (error) { send(500, { error: (error as Error).message.slice(0, 200) }); }\n };\n // The pipe or socket first. A Unix socket's folder is created for this user only, and the socket itself is too.\n const socketPath = localSocketPath(dir);\n const local = createServer(handle);\n let socket: string | null = socketPath;\n try {\n if (process.platform !== \"win32\") {\n mkdirSync(join(socketPath, \"..\"), { recursive: true, mode: 0o700 });\n chmodSync(join(socketPath, \"..\"), 0o700);\n rmSync(socketPath, { force: true });\n }\n await new Promise<void>((resolve, reject) => { local.once(\"error\", reject); local.listen(socketPath, () => resolve()); });\n if (process.platform !== \"win32\") chmodSync(socketPath, 0o600);\n } catch { socket = null; }\n // Loopback for one more release (the PowerShell tray), unless turned off; always when the pipe could not be made.\n const loopback = process.env.SCOPEBOND_AGENT_LOOPBACK !== \"0\" || socket === null;\n const server = createServer(handle);\n if (loopback) await new Promise<void>((resolve, reject) => { server.once(\"error\", reject); server.listen(0, \"127.0.0.1\", () => resolve()); });\n const address = loopback ? server.address() : null;\n const port = typeof address === \"object\" && address ? address.port : 0;\n const endpoint: AgentEndpoint = { port, socket, token, pid: process.pid, started_at: Date.now(), version };\n const file = join(dir, AGENT_FILE);\n const temp = `${file}.${process.pid}.tmp`;\n writeFileSync(temp, JSON.stringify(endpoint, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, file);\n return {\n server, endpoint,\n close: () => new Promise<void>((resolve) => {\n let open = (loopback ? 1 : 0) + (socket ? 1 : 0);\n const done = () => { if (--open <= 0) resolve(); };\n if (open === 0) resolve();\n if (loopback) server.close(done);\n if (socket) local.close(() => { if (process.platform !== \"win32\") rmSync(socketPath, { force: true }); done(); });\n // Only remove the file if it is still ours (a newer agent may have replaced it).\n try { if (readEndpoint(dir)?.pid === process.pid) rmSync(file, { force: true }); } catch { /* best effort */ }\n }),\n };\n}\n\n/** Call a running agent, over its pipe or socket when it has one. Returns null when no agent answers. */\nexport async function callAgent(dir: string, method: \"GET\" | \"POST\", path: string, body?: unknown, timeoutMs = 5_000): Promise<unknown | null> {\n const endpoint = readEndpoint(dir);\n if (!endpoint) return null;\n if (typeof endpoint.socket === \"string\" && endpoint.socket) {\n const answer = await requestOverSocket(endpoint.socket, method, path, { [TOKEN_HEADER]: endpoint.token }, body, timeoutMs);\n if (answer) return answer.status >= 200 && answer.status < 300 ? answer.body : null;\n if (!endpoint.port) return null;\n }\n try {\n const res = await fetch(`http://127.0.0.1:${endpoint.port}${path}`, {\n method, headers: { [TOKEN_HEADER]: endpoint.token, ...(body !== undefined ? { \"content-type\": \"application/json\" } : {}) },\n body: body !== undefined ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeoutMs),\n });\n return res.ok ? await res.json() : null;\n } catch { return null; }\n}\n"]}
@@ -81,6 +81,10 @@ export declare const OFFLINE_ATTENTION_MS: number;
81
81
  export declare const RULES_STALE_MS: number;
82
82
  /** A delivery error that means the workspace could not be reached (no answer at all), not a refusal. */
83
83
  export declare function unreachable(error: string | null | undefined): boolean;
84
+ /** The workspace refused records because it reached its monthly limit (they stay on the computer). */
85
+ export declare function overQuota(error: string | null | undefined): boolean;
86
+ /** The HTTP status of a refusal the workspace answered (not "unreachable"), or null. */
87
+ export declare function refusedStatus(error: string | null | undefined): number | null;
84
88
  export declare function ago(at: number, now: number): string;
85
89
  export declare function trayModel(input: TrayInput): TrayModel;
86
90
  /** What "Check now" says afterwards, for five seconds (SB387): never silence. */
@@ -1 +1 @@
1
- {"version":3,"file":"tray-model.d.ts","sourceRoot":"","sources":["../src/tray-model.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,iHAAiH;AACjH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AAEvG,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,UAAU,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,gBAAgB,CAAC;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,CAAC;IACjB,qGAAqG;IACrG,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,IAAI,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,wFAAwF;IACxF,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACvB,6CAA6C;IAC7C,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,WAAW,EAAE,CAAC;CAC9B;AAED,6HAA6H;AAC7H,MAAM,WAAW,WAAW;IAAG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;CAAE;AAElK,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,2GAA2G;IAC3G,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9F,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9E,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtJ,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACnG,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+GAA+G;IAC/G,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAID,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,QAAW,CAAC;AAC7C,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,QAAW,CAAC;AAC7C,mEAAmE;AACnE,eAAO,MAAM,cAAc,QAAO,CAAC;AAEnC,wGAAwG;AACxG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAErE;AAED,wBAAgB,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQnD;AAuBD,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CA6ErD;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,SAAS,EAAE;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAI7G"}
1
+ {"version":3,"file":"tray-model.d.ts","sourceRoot":"","sources":["../src/tray-model.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,15 @@ export const RULES_STALE_MS = HOUR;
13
13
  export function unreachable(error) {
14
14
  return !!error && !/HTTP \d{3}/.test(error) && /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|getaddrinfo|network|socket|could not reach/i.test(error);
15
15
  }
16
+ /** The workspace refused records because it reached its monthly limit (they stay on the computer). */
17
+ export function overQuota(error) {
18
+ return !!error && /HTTP 429 \(quota\)/.test(error);
19
+ }
20
+ /** The HTTP status of a refusal the workspace answered (not "unreachable"), or null. */
21
+ export function refusedStatus(error) {
22
+ const m = error ? /HTTP (\d{3})/.exec(error) : null;
23
+ return m ? Number(m[1]) : null;
24
+ }
16
25
  export function ago(at, now) {
17
26
  const s = Math.max(0, Math.round((now - at) / 1000));
18
27
  if (s < 60)
@@ -72,6 +81,13 @@ export function trayModel(input) {
72
81
  else
73
82
  hint = health.hint;
74
83
  }
84
+ else if (pending > 0 && overQuota(d.last_error) && health.fix?.route !== "/repair") {
85
+ // Not "sending": the workspace refuses them until its limit allows. Nothing is lost; say so, and where it is changed.
86
+ state = "attention";
87
+ headline = `Workspace limit reached: ${pending} record${pending === 1 ? "" : "s"} waiting`;
88
+ hint = "Your workspace reached its monthly limit. Records stay on this computer and send once the limit allows; a workspace owner can change the plan.";
89
+ fix = ACTIONS.open_workspace;
90
+ }
75
91
  else if (health.level === "red") {
76
92
  state = "problem";
77
93
  headline = health.headline;
@@ -113,7 +129,9 @@ export function trayModel(input) {
113
129
  const value = pending === 0
114
130
  ? (d.last_success_at ? `All sent · ${ago(d.last_success_at, now)}` : "Nothing to send yet")
115
131
  : offline ? `${pending} waiting since ${oldestAt ? clock(oldestAt) : "earlier"} · offline`
116
- : `${pending} waiting · sending`;
132
+ : overQuota(d.last_error) ? `${pending} waiting · workspace limit reached`
133
+ : refusedStatus(d.last_error) ? `${pending} waiting · last try refused (HTTP ${refusedStatus(d.last_error)})`
134
+ : `${pending} waiting · sending`;
117
135
  rows.push({ label: "Delivery", value });
118
136
  }
119
137
  if (input.today) {
@@ -1 +1 @@
1
- {"version":3,"file":"tray-model.js","sourceRoot":"","sources":["../src/tray-model.ts"],"names":[],"mappings":"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","sourcesContent":["// What the tray shows, as data (SB387, SB388). The tray (today a PowerShell script, later a native one) only draws this: one\n// state, one headline, the rows that have data, the one fix when something is wrong, and the actions that apply now. It never\n// works out health itself, so the tray, `status` and the workspace's computer page say the same thing.\n\nimport type { StatusJson } from \"@scopebond/hook\";\nimport type { Health } from \"./health.js\";\n\n/** The six icon states of the \"S\" tile (D143): no badge when protected; a badge with its own glyph otherwise. */\nexport type TrayState = \"protected\" | \"working\" | \"offline\" | \"attention\" | \"problem\" | \"disconnected\";\n\nexport interface TrayAction {\n id: \"send_now\" | \"update_now\" | \"check_now\" | \"repair\" | \"reconnect\" | \"open_workspace\";\n label: string;\n /** The agent's local route that does it (POST). */\n route: string;\n}\n\nexport interface TrayModel {\n state: TrayState;\n /** One line at the top, and the tooltip (Windows allows 63 characters in a classic tray tooltip). */\n headline: string;\n tooltip: string;\n /** Rules, Delivery, Today, Version: each only when there is data (never \"unknown\"). */\n rows: Array<{ label: string; value: string }>;\n /** The one fix when something is wrong (shown first, in place of the usual actions). */\n fix: TrayAction | null;\n /** Actions that apply now, in menu order. */\n actions: TrayAction[];\n /** A sign-in or similar step no button can do, in words. */\n hint: string | null;\n recent_blocks: RecentBlock[];\n}\n\n/** A recent block. `can_act`: the person may still allow it or ask an admin from the tray (D144); `acted`: what they did. */\nexport interface RecentBlock { action_id: string | null; summary: string; at: string; rule: string | null; can_act?: boolean; acted?: \"allowed\" | \"asked\" | null }\n\nexport interface TrayInput {\n status: StatusJson;\n health: Health;\n now: number;\n /** When this computer last came back from sleep or started (the agent sees the gap between its cycles). */\n awakeSince: number;\n /** A long step the agent is running (an update, a sign-in, a repair), or null. */\n working: string | null;\n rules: { checked_at: number | null; managed: boolean; block: number; monitor: number } | null;\n today: { actions: number; blocked: number; allowed_by_person: number } | null;\n recentBlocks?: RecentBlock[];\n version: { agent: string; hook: string; policy: \"recommended\" | \"hold\" | \"unknown\"; recommendedAgent: string | null; recommendedHook: string | null };\n workspace: { name: string | null; environment: string | null; computer_url: string | null } | null;\n computerName: string;\n /** Items about this computer waiting in the workspace's Review (from the workspace's summary). */\n openReviews?: number;\n /** Whether the agent can run the sign-in itself (`POST /reconnect`); until then the tray shows the command. */\n canReconnect?: boolean;\n}\n\nconst MIN = 60_000;\nconst HOUR = 60 * MIN;\n/** Records waiting this long while the computer is awake and online need attention (D143 §4.1). */\nexport const WAITING_ATTENTION_MS = 15 * MIN;\n/** Offline (the workspace unreachable) this long needs attention. */\nexport const OFFLINE_ATTENTION_MS = 4 * HOUR;\n/** Rules not checked for this long while online need attention. */\nexport const RULES_STALE_MS = HOUR;\n\n/** A delivery error that means the workspace could not be reached (no answer at all), not a refusal. */\nexport function unreachable(error: string | null | undefined): boolean {\n return !!error && !/HTTP \\d{3}/.test(error) && /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|getaddrinfo|network|socket|could not reach/i.test(error);\n}\n\nexport function ago(at: number, now: number): string {\n const s = Math.max(0, Math.round((now - at) / 1000));\n if (s < 60) return \"just now\";\n const m = Math.round(s / 60);\n if (m < 60) return `${m} min ago`;\n const h = Math.round(m / 60);\n if (h < 36) return `${h} h ago`;\n return `${Math.round(h / 24)} days ago`;\n}\n\nfunction clock(at: number): string {\n const d = new Date(at);\n return `${String(d.getHours()).padStart(2, \"0\")}:${String(d.getMinutes()).padStart(2, \"0\")}`;\n}\n\nconst newer = (want: string | null, have: string): boolean => {\n if (!want) return false;\n const a = want.split(\".\").map(Number), b = have.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);\n return false;\n};\n\nconst ACTIONS: Record<TrayAction[\"id\"], TrayAction> = {\n send_now: { id: \"send_now\", label: \"Send records now\", route: \"/flush\" },\n update_now: { id: \"update_now\", label: \"Update now\", route: \"/update\" },\n check_now: { id: \"check_now\", label: \"Check now\", route: \"/check\" },\n repair: { id: \"repair\", label: \"Repair\", route: \"/repair\" },\n reconnect: { id: \"reconnect\", label: \"Reconnect…\", route: \"/reconnect\" },\n open_workspace: { id: \"open_workspace\", label: \"Open workspace\", route: \"/open-workspace\" },\n};\n\nexport function trayModel(input: TrayInput): TrayModel {\n const { status, health, now } = input;\n const d = status.delivery;\n const pending = d.pending;\n const oldestAt = d.oldest_pending_age_s === null ? null : now - d.oldest_pending_age_s * 1000;\n // Sleep-aware (SB388): time the computer was asleep or off never counts as waiting.\n const waitingMs = oldestAt === null ? 0 : now - Math.max(oldestAt, input.awakeSince);\n const offline = d.connected && unreachable(d.last_error);\n const offlineFor = offline ? now - Math.max(d.last_success_at ?? input.awakeSince, input.awakeSince) : 0;\n const updateAvailable = input.version.policy === \"recommended\" && (newer(input.version.recommendedAgent, input.version.agent) || newer(input.version.recommendedHook, input.version.hook));\n\n let state: TrayState;\n let headline: string;\n let fix: TrayAction | null = null;\n let hint: string | null = null;\n if (input.working) {\n state = \"working\"; headline = input.working;\n } else if (health.level === \"red\" && d.connection_refused_since !== null) {\n state = \"disconnected\"; headline = \"Not connected to your workspace: using this computer's own rules\";\n if (input.canReconnect) fix = ACTIONS.reconnect; else hint = health.hint;\n } else if (health.level === \"red\") {\n state = \"problem\"; headline = health.headline; hint = health.hint;\n if (health.fix?.route === \"/repair\") fix = ACTIONS.repair;\n } else if (offline) {\n state = offlineFor > OFFLINE_ATTENTION_MS ? \"attention\" : \"offline\";\n headline = pending ? `Workspace unreachable: ${pending} record${pending === 1 ? \"\" : \"s\"} waiting` : \"Workspace unreachable: still checking actions\";\n } else if (pending > 0 && waitingMs > WAITING_ATTENTION_MS) {\n state = \"attention\"; headline = `${pending} record${pending === 1 ? \"\" : \"s\"} waiting to send`; fix = ACTIONS.send_now;\n } else if (health.level === \"amber\" && health.fix?.route === \"/maintain\") {\n state = \"attention\"; headline = health.headline; fix = ACTIONS.check_now;\n } else if (input.rules?.checked_at && d.connected && now - input.rules.checked_at > RULES_STALE_MS) {\n state = \"attention\"; headline = `Rules not checked for ${ago(input.rules.checked_at, now).replace(\" ago\", \"\")}`; fix = ACTIONS.check_now;\n } else {\n state = \"protected\"; headline = \"Protected\";\n }\n\n const rows: TrayModel[\"rows\"] = [];\n if (input.rules) {\n const when = input.rules.checked_at ? ` · checked ${ago(input.rules.checked_at, now)}` : \"\";\n const source = input.rules.managed ? \"Up to date\" : \"Using this computer's own rules\";\n const counts = input.rules.block + input.rules.monitor > 0 ? ` · ${input.rules.block} block · ${input.rules.monitor} monitor` : \"\";\n rows.push({ label: \"Rules\", value: `${input.rules.managed && input.rules.checked_at && now - input.rules.checked_at > RULES_STALE_MS ? `Not checked for ${ago(input.rules.checked_at, now).replace(\" ago\", \"\")}` : source}${when}${counts}` });\n }\n if (d.connected) {\n const value = pending === 0\n ? (d.last_success_at ? `All sent · ${ago(d.last_success_at, now)}` : \"Nothing to send yet\")\n : offline ? `${pending} waiting since ${oldestAt ? clock(oldestAt) : \"earlier\"} · offline`\n : `${pending} waiting · sending`;\n rows.push({ label: \"Delivery\", value });\n }\n if (input.today) {\n const parts = [`${input.today.actions} action${input.today.actions === 1 ? \"\" : \"s\"}`, `${input.today.blocked} blocked`];\n if (input.today.allowed_by_person) parts.push(`${input.today.allowed_by_person} allowed by a person`);\n if (input.openReviews) parts.push(`${input.openReviews} in Review`);\n rows.push({ label: \"Today\", value: parts.join(\" · \") });\n }\n rows.push({\n label: \"Version\",\n value: updateAvailable ? `Update available (agent ${input.version.recommendedAgent ?? input.version.agent})`\n : input.version.policy === \"hold\" ? `agent ${input.version.agent} · updates managed by your workspace`\n : `Up to date (agent ${input.version.agent})`,\n });\n\n const actions: TrayAction[] = [];\n if (pending > 0 && fix?.id !== \"send_now\" && state !== \"disconnected\") actions.push(ACTIONS.send_now);\n if (updateAvailable && !input.working) actions.push(ACTIONS.update_now);\n if (fix?.id !== \"check_now\") actions.push(ACTIONS.check_now);\n if (input.workspace?.computer_url) actions.push(ACTIONS.open_workspace);\n\n const where = [input.workspace?.name, input.workspace?.environment].filter(Boolean).join(\" · \");\n const label = state === \"protected\" ? `Protected${where ? ` · ${where}` : \"\"}` : headline;\n const tooltip = `Scopebond — ${label}`;\n return {\n state, headline, fix, actions, hint, rows,\n tooltip: tooltip.length > 63 ? `${tooltip.slice(0, 62)}…` : tooltip,\n recent_blocks: (input.recentBlocks ?? []).slice(0, 3),\n };\n}\n\n/** What \"Check now\" says afterwards, for five seconds (SB387): never silence. */\nexport function checkResult(selfCheck: { ok: boolean; failed: string[] } | null, error: string | null): string {\n if (error) return `Check could not finish: ${error}`.slice(0, 200);\n if (!selfCheck) return \"Checked just now: this computer is not connected to a workspace, so only local checks ran\";\n return selfCheck.ok ? \"Checked just now: all good\" : `Check found a problem: ${selfCheck.failed[0] ?? \"see status\"}`;\n}\n"]}
1
+ {"version":3,"file":"tray-model.js","sourceRoot":"","sources":["../src/tray-model.ts"],"names":[],"mappings":"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","sourcesContent":["// What the tray shows, as data (SB387, SB388). The tray (today a PowerShell script, later a native one) only draws this: one\n// state, one headline, the rows that have data, the one fix when something is wrong, and the actions that apply now. It never\n// works out health itself, so the tray, `status` and the workspace's computer page say the same thing.\n\nimport type { StatusJson } from \"@scopebond/hook\";\nimport type { Health } from \"./health.js\";\n\n/** The six icon states of the \"S\" tile (D143): no badge when protected; a badge with its own glyph otherwise. */\nexport type TrayState = \"protected\" | \"working\" | \"offline\" | \"attention\" | \"problem\" | \"disconnected\";\n\nexport interface TrayAction {\n id: \"send_now\" | \"update_now\" | \"check_now\" | \"repair\" | \"reconnect\" | \"open_workspace\";\n label: string;\n /** The agent's local route that does it (POST). */\n route: string;\n}\n\nexport interface TrayModel {\n state: TrayState;\n /** One line at the top, and the tooltip (Windows allows 63 characters in a classic tray tooltip). */\n headline: string;\n tooltip: string;\n /** Rules, Delivery, Today, Version: each only when there is data (never \"unknown\"). */\n rows: Array<{ label: string; value: string }>;\n /** The one fix when something is wrong (shown first, in place of the usual actions). */\n fix: TrayAction | null;\n /** Actions that apply now, in menu order. */\n actions: TrayAction[];\n /** A sign-in or similar step no button can do, in words. */\n hint: string | null;\n recent_blocks: RecentBlock[];\n}\n\n/** A recent block. `can_act`: the person may still allow it or ask an admin from the tray (D144); `acted`: what they did. */\nexport interface RecentBlock { action_id: string | null; summary: string; at: string; rule: string | null; can_act?: boolean; acted?: \"allowed\" | \"asked\" | null }\n\nexport interface TrayInput {\n status: StatusJson;\n health: Health;\n now: number;\n /** When this computer last came back from sleep or started (the agent sees the gap between its cycles). */\n awakeSince: number;\n /** A long step the agent is running (an update, a sign-in, a repair), or null. */\n working: string | null;\n rules: { checked_at: number | null; managed: boolean; block: number; monitor: number } | null;\n today: { actions: number; blocked: number; allowed_by_person: number } | null;\n recentBlocks?: RecentBlock[];\n version: { agent: string; hook: string; policy: \"recommended\" | \"hold\" | \"unknown\"; recommendedAgent: string | null; recommendedHook: string | null };\n workspace: { name: string | null; environment: string | null; computer_url: string | null } | null;\n computerName: string;\n /** Items about this computer waiting in the workspace's Review (from the workspace's summary). */\n openReviews?: number;\n /** Whether the agent can run the sign-in itself (`POST /reconnect`); until then the tray shows the command. */\n canReconnect?: boolean;\n}\n\nconst MIN = 60_000;\nconst HOUR = 60 * MIN;\n/** Records waiting this long while the computer is awake and online need attention (D143 §4.1). */\nexport const WAITING_ATTENTION_MS = 15 * MIN;\n/** Offline (the workspace unreachable) this long needs attention. */\nexport const OFFLINE_ATTENTION_MS = 4 * HOUR;\n/** Rules not checked for this long while online need attention. */\nexport const RULES_STALE_MS = HOUR;\n\n/** A delivery error that means the workspace could not be reached (no answer at all), not a refusal. */\nexport function unreachable(error: string | null | undefined): boolean {\n return !!error && !/HTTP \\d{3}/.test(error) && /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|getaddrinfo|network|socket|could not reach/i.test(error);\n}\n\n/** The workspace refused records because it reached its monthly limit (they stay on the computer). */\nexport function overQuota(error: string | null | undefined): boolean {\n return !!error && /HTTP 429 \\(quota\\)/.test(error);\n}\n\n/** The HTTP status of a refusal the workspace answered (not \"unreachable\"), or null. */\nexport function refusedStatus(error: string | null | undefined): number | null {\n const m = error ? /HTTP (\\d{3})/.exec(error) : null;\n return m ? Number(m[1]) : null;\n}\n\nexport function ago(at: number, now: number): string {\n const s = Math.max(0, Math.round((now - at) / 1000));\n if (s < 60) return \"just now\";\n const m = Math.round(s / 60);\n if (m < 60) return `${m} min ago`;\n const h = Math.round(m / 60);\n if (h < 36) return `${h} h ago`;\n return `${Math.round(h / 24)} days ago`;\n}\n\nfunction clock(at: number): string {\n const d = new Date(at);\n return `${String(d.getHours()).padStart(2, \"0\")}:${String(d.getMinutes()).padStart(2, \"0\")}`;\n}\n\nconst newer = (want: string | null, have: string): boolean => {\n if (!want) return false;\n const a = want.split(\".\").map(Number), b = have.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);\n return false;\n};\n\nconst ACTIONS: Record<TrayAction[\"id\"], TrayAction> = {\n send_now: { id: \"send_now\", label: \"Send records now\", route: \"/flush\" },\n update_now: { id: \"update_now\", label: \"Update now\", route: \"/update\" },\n check_now: { id: \"check_now\", label: \"Check now\", route: \"/check\" },\n repair: { id: \"repair\", label: \"Repair\", route: \"/repair\" },\n reconnect: { id: \"reconnect\", label: \"Reconnect…\", route: \"/reconnect\" },\n open_workspace: { id: \"open_workspace\", label: \"Open workspace\", route: \"/open-workspace\" },\n};\n\nexport function trayModel(input: TrayInput): TrayModel {\n const { status, health, now } = input;\n const d = status.delivery;\n const pending = d.pending;\n const oldestAt = d.oldest_pending_age_s === null ? null : now - d.oldest_pending_age_s * 1000;\n // Sleep-aware (SB388): time the computer was asleep or off never counts as waiting.\n const waitingMs = oldestAt === null ? 0 : now - Math.max(oldestAt, input.awakeSince);\n const offline = d.connected && unreachable(d.last_error);\n const offlineFor = offline ? now - Math.max(d.last_success_at ?? input.awakeSince, input.awakeSince) : 0;\n const updateAvailable = input.version.policy === \"recommended\" && (newer(input.version.recommendedAgent, input.version.agent) || newer(input.version.recommendedHook, input.version.hook));\n\n let state: TrayState;\n let headline: string;\n let fix: TrayAction | null = null;\n let hint: string | null = null;\n if (input.working) {\n state = \"working\"; headline = input.working;\n } else if (health.level === \"red\" && d.connection_refused_since !== null) {\n state = \"disconnected\"; headline = \"Not connected to your workspace: using this computer's own rules\";\n if (input.canReconnect) fix = ACTIONS.reconnect; else hint = health.hint;\n } else if (pending > 0 && overQuota(d.last_error) && health.fix?.route !== \"/repair\") {\n // Not \"sending\": the workspace refuses them until its limit allows. Nothing is lost; say so, and where it is changed.\n state = \"attention\"; headline = `Workspace limit reached: ${pending} record${pending === 1 ? \"\" : \"s\"} waiting`;\n hint = \"Your workspace reached its monthly limit. Records stay on this computer and send once the limit allows; a workspace owner can change the plan.\";\n fix = ACTIONS.open_workspace;\n } else if (health.level === \"red\") {\n state = \"problem\"; headline = health.headline; hint = health.hint;\n if (health.fix?.route === \"/repair\") fix = ACTIONS.repair;\n } else if (offline) {\n state = offlineFor > OFFLINE_ATTENTION_MS ? \"attention\" : \"offline\";\n headline = pending ? `Workspace unreachable: ${pending} record${pending === 1 ? \"\" : \"s\"} waiting` : \"Workspace unreachable: still checking actions\";\n } else if (pending > 0 && waitingMs > WAITING_ATTENTION_MS) {\n state = \"attention\"; headline = `${pending} record${pending === 1 ? \"\" : \"s\"} waiting to send`; fix = ACTIONS.send_now;\n } else if (health.level === \"amber\" && health.fix?.route === \"/maintain\") {\n state = \"attention\"; headline = health.headline; fix = ACTIONS.check_now;\n } else if (input.rules?.checked_at && d.connected && now - input.rules.checked_at > RULES_STALE_MS) {\n state = \"attention\"; headline = `Rules not checked for ${ago(input.rules.checked_at, now).replace(\" ago\", \"\")}`; fix = ACTIONS.check_now;\n } else {\n state = \"protected\"; headline = \"Protected\";\n }\n\n const rows: TrayModel[\"rows\"] = [];\n if (input.rules) {\n const when = input.rules.checked_at ? ` · checked ${ago(input.rules.checked_at, now)}` : \"\";\n const source = input.rules.managed ? \"Up to date\" : \"Using this computer's own rules\";\n const counts = input.rules.block + input.rules.monitor > 0 ? ` · ${input.rules.block} block · ${input.rules.monitor} monitor` : \"\";\n rows.push({ label: \"Rules\", value: `${input.rules.managed && input.rules.checked_at && now - input.rules.checked_at > RULES_STALE_MS ? `Not checked for ${ago(input.rules.checked_at, now).replace(\" ago\", \"\")}` : source}${when}${counts}` });\n }\n if (d.connected) {\n const value = pending === 0\n ? (d.last_success_at ? `All sent · ${ago(d.last_success_at, now)}` : \"Nothing to send yet\")\n : offline ? `${pending} waiting since ${oldestAt ? clock(oldestAt) : \"earlier\"} · offline`\n : overQuota(d.last_error) ? `${pending} waiting · workspace limit reached`\n : refusedStatus(d.last_error) ? `${pending} waiting · last try refused (HTTP ${refusedStatus(d.last_error)})`\n : `${pending} waiting · sending`;\n rows.push({ label: \"Delivery\", value });\n }\n if (input.today) {\n const parts = [`${input.today.actions} action${input.today.actions === 1 ? \"\" : \"s\"}`, `${input.today.blocked} blocked`];\n if (input.today.allowed_by_person) parts.push(`${input.today.allowed_by_person} allowed by a person`);\n if (input.openReviews) parts.push(`${input.openReviews} in Review`);\n rows.push({ label: \"Today\", value: parts.join(\" · \") });\n }\n rows.push({\n label: \"Version\",\n value: updateAvailable ? `Update available (agent ${input.version.recommendedAgent ?? input.version.agent})`\n : input.version.policy === \"hold\" ? `agent ${input.version.agent} · updates managed by your workspace`\n : `Up to date (agent ${input.version.agent})`,\n });\n\n const actions: TrayAction[] = [];\n if (pending > 0 && fix?.id !== \"send_now\" && state !== \"disconnected\") actions.push(ACTIONS.send_now);\n if (updateAvailable && !input.working) actions.push(ACTIONS.update_now);\n if (fix?.id !== \"check_now\") actions.push(ACTIONS.check_now);\n if (input.workspace?.computer_url) actions.push(ACTIONS.open_workspace);\n\n const where = [input.workspace?.name, input.workspace?.environment].filter(Boolean).join(\" · \");\n const label = state === \"protected\" ? `Protected${where ? ` · ${where}` : \"\"}` : headline;\n const tooltip = `Scopebond — ${label}`;\n return {\n state, headline, fix, actions, hint, rows,\n tooltip: tooltip.length > 63 ? `${tooltip.slice(0, 62)}…` : tooltip,\n recent_blocks: (input.recentBlocks ?? []).slice(0, 3),\n };\n}\n\n/** What \"Check now\" says afterwards, for five seconds (SB387): never silence. */\nexport function checkResult(selfCheck: { ok: boolean; failed: string[] } | null, error: string | null): string {\n if (error) return `Check could not finish: ${error}`.slice(0, 200);\n if (!selfCheck) return \"Checked just now: this computer is not connected to a workspace, so only local checks ran\";\n return selfCheck.ok ? \"Checked just now: all good\" : `Check found a problem: ${selfCheck.failed[0] ?? \"see status\"}`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scopebond/agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Free, open-source Scopebond Agent companion: deliver queued records, maintain rules and connections, and report computer health to the separate proprietary Scopebond Cloud workspace.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Avouro LLC",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@scopebond/gateway": "0.17.0",
43
- "@scopebond/hook": "0.21.0",
43
+ "@scopebond/hook": "0.21.1",
44
44
  "@scopebond/sdk": "0.1.5"
45
45
  },
46
46
  "publishConfig": {