grix-connector 3.3.2 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,6 +142,47 @@ grix-connector start
142
142
 
143
143
  The daemon connects to Grix via WebSocket and starts routing chat messages to your agents.
144
144
 
145
+ > The daemon refuses to start when the config directory holds no valid agent config, so on a first-time setup the config file must be written **before** `start`.
146
+
147
+ ### Adding an agent to an existing setup
148
+
149
+ If `~/.grix/config/agents.json` already exists and other agents are running, **do not overwrite the file** — merge the new entry into it.
150
+
151
+ **1. Merge the entry.** Read the file as JSON and look through the `agents` array for an entry whose `agent_id` matches the one you are adding:
152
+
153
+ - found → replace that entry
154
+ - not found → append the new entry
155
+ - every other entry must be left exactly as it was
156
+
157
+ Edit it with a script (`node`, `python3`), not by hand — a truncated or re-indented file silently drops the agents you did not intend to touch. If the file does not exist yet, create it with a single-element `agents` array.
158
+
159
+ **2. Apply the change.** Check whether the daemon is already running:
160
+
161
+ ```bash
162
+ grix-connector status
163
+ ```
164
+
165
+ - not running (or it reports `daemon is not running`) → `grix-connector start`
166
+ - already running → `grix-connector reload`
167
+
168
+ `reload` hot-loads the new agent and leaves every already-running agent untouched — their live sessions are not interrupted. Do not use `restart` to add an agent: it reconnects everything and drops all in-flight conversations.
169
+
170
+ **3. Verify.** `grix-connector status` reports the daemon only — it does not list agents. To confirm the new agent is connected, query the local admin API (give it a few seconds after `start`/`reload`):
171
+
172
+ ```bash
173
+ curl -s http://127.0.0.1:19580/api/agents
174
+ ```
175
+
176
+ The entry for your agent should report `"alive": true`. `19580` is the default admin port; if it was overridden (`--admin-port` or `GRIX_ADMIN_PORT`), the port actually in use is written to `~/.grix/data/admin-port`.
177
+
178
+ If it never connects, read the newest log under `~/.grix/log/`. In practice it is almost always one of three things:
179
+
180
+ - the CLI for that `client_type` is not on `PATH`
181
+ - the CLI is installed but not logged in
182
+ - the `api_key` was truncated when it was copied
183
+
184
+ **Credentials.** `api_key` is a one-time secret: write it into `~/.grix/config/agents.json` and nowhere else. Do not echo it into logs or commit it to a repository.
185
+
145
186
  ### Commands
146
187
 
147
188
  ```bash
@@ -224,11 +265,34 @@ Or manually add to your OpenClaw project:
224
265
  npm install grix-connector
225
266
  ```
226
267
 
268
+ ### Connecting an agent (plugin mode)
269
+
270
+ Plugin mode does **not** use `~/.grix/config/agents.json`. The credentials live in OpenClaw's own config (`openclaw.json`) under `channels.grix`, and the field names are camelCase — `wsUrl` / `agentId` / `apiKey` — not the snake_case names used by the daemon.
271
+
272
+ Write them with `openclaw config set`. **Do not hand-edit `openclaw.json`.**
273
+
274
+ ```bash
275
+ openclaw config set channels.grix.accounts.<account-id> \
276
+ '{"name":"my-agent","enabled":true,"wsUrl":"wss://grix.dhf.pub/v1/agent-api/ws","agentId":"your-agent-id","apiKey":"your-grix-api-key"}' \
277
+ --strict-json
278
+ ```
279
+
280
+ `<account-id>` is a name you choose (lowercase letters, digits, hyphens). Use the `ws.grix.im` domain instead for the global region. If `channels.grix.enabled` was explicitly turned off before, also run `openclaw config set channels.grix.enabled true --strict-json`.
281
+
282
+ Then check it:
283
+
284
+ ```bash
285
+ openclaw config validate
286
+ openclaw grix doctor # the account should report configured: true, enabled: true
287
+ ```
288
+
289
+ Finally send a message to the agent from Grix and confirm the reply really comes from it. Only if the config validates but messages do not route should you run `openclaw gateway restart` — once — and then re-send the message.
290
+
227
291
  ### Plugin Features
228
292
 
229
293
  - **Channel**: Grix chat transport — routes messages between OpenClaw and your Grix deployment
230
294
  - **Tools**: `grix_query`, `grix_group`, `grix_admin`, `grix_egg`, `grix_register`, `grix_update`, `grix_message_send`, `grix_message_unsend`, `openclaw_memory_setup`
231
- - **CLI**: `openclaw grix` — agent management and admin commands
295
+ - **CLI**: `openclaw grix doctor` — local diagnostics (the only subcommand; configuration goes through `openclaw config set`)
232
296
  - **Skills**: 9 bundled skills for admin, group, query, registration, update, messaging, memory setup, and egg orchestration
233
297
 
234
298
  ### Requirements
@@ -236,6 +300,8 @@ npm install grix-connector
236
300
  - OpenClaw >= 2026.4.8
237
301
  - A Grix account with agent ID and API key
238
302
 
303
+ Plugin mode and the standalone daemon can coexist on one machine — they use separate configs and separate processes. Do not, however, bring the **same** `agent_id` online in both at once; give each mode its own agent.
304
+
239
305
  ## License
240
306
 
241
307
  MIT
@@ -1 +1 @@
1
- import{readAllowlist as a,writeAllowlist as r}from"./allowlist-store.js";import{log as s}from"../log/index.js";class u{filePath;writeQueue=Promise.resolve();constructor(e){this.filePath=e}async checkAccess(e){if(!e?.trim())return!1;const t=a(this.filePath);return t.includes(e)?!0:t.length===0?this.autoAdd(e):!1}async addOwner(e){e?.trim()&&(this.writeQueue=this.writeQueue.then(async()=>{const t=a(this.filePath);t.includes(e)||(t.push(e),await r(this.filePath,t))}).catch(t=>{s.error("allowlist-gate",`addOwner failed for ${e}: ${t instanceof Error?t.message:t}`)}),await this.writeQueue)}async removeOwner(e){e?.trim()&&(this.writeQueue=this.writeQueue.then(async()=>{const t=a(this.filePath),i=t.filter(l=>l!==e);i.length!==t.length&&await r(this.filePath,i)}).catch(t=>{s.error("allowlist-gate",`removeOwner failed for ${e}: ${t instanceof Error?t.message:t}`)}),await this.writeQueue)}async listOwners(){return a(this.filePath)}autoAdd(e){return new Promise(t=>{this.writeQueue=this.writeQueue.then(async()=>{const i=a(this.filePath);if(i.includes(e)){t(!0);return}if(i.length>0){t(!1);return}await r(this.filePath,[e]),t(!0)}).catch(i=>{s.error("allowlist-gate",`autoAdd failed for ${e}: ${i instanceof Error?i.message:i}`),t(!1)})})}}export{u as AllowlistGate};
1
+ import{readAllowlist as r,writeAllowlist as s}from"./allowlist-store.js";import{log as a}from"../log/index.js";class h{filePath;writeQueue=Promise.resolve();constructor(e){this.filePath=e}async checkAccess(e){if(!e?.trim())return!1;const t=r(this.filePath);return t.includes(e)?!0:t.length===0?this.autoAdd(e):!1}async addOwner(e){e?.trim()&&(this.writeQueue=this.writeQueue.then(async()=>{const t=r(this.filePath);t.includes(e)||(t.push(e),await s(this.filePath,t))}).catch(t=>{a.error("allowlist-gate",`addOwner failed for ${e}: ${t instanceof Error?t.message:t}`)}),await this.writeQueue)}async removeOwner(e){e?.trim()&&(this.writeQueue=this.writeQueue.then(async()=>{const t=r(this.filePath),i=t.filter(l=>l!==e);i.length!==t.length&&await s(this.filePath,i)}).catch(t=>{a.error("allowlist-gate",`removeOwner failed for ${e}: ${t instanceof Error?t.message:t}`)}),await this.writeQueue)}async listOwners(){return r(this.filePath)}autoAdd(e){return new Promise(t=>{this.writeQueue=this.writeQueue.then(async()=>{const i=r(this.filePath);if(i.includes(e)){t(!0);return}if(i.length>0){t(!1);return}await s(this.filePath,[e]),t(!0)}).catch(i=>{a.error("allowlist-gate",`autoAdd failed for ${e}: ${i instanceof Error?i.message:i}`),t(!1)})})}}export{h as AllowlistGate};
@@ -1 +1 @@
1
- import{readJSONFile as i,writeJSONFileAtomic as o}from"../util/json-file.js";function l(t){const r=i(t);return r&&typeof r=="object"&&"owners"in r&&Array.isArray(r.owners)?r.owners.filter(e=>typeof e=="string"&&e.trim().length>0):[]}async function a(t,r){await o(t,{owners:r})}export{l as readAllowlist,a as writeAllowlist};
1
+ import{readJSONFile as o,writeJSONFileAtomic as i}from"../util/json-file.js";function n(t){const r=o(t);return r&&typeof r=="object"&&"owners"in r&&Array.isArray(r.owners)?r.owners.filter(e=>typeof e=="string"&&e.trim().length>0):[]}async function s(t,r){await i(t,{owners:r})}export{n as readAllowlist,s as writeAllowlist};
@@ -1 +1 @@
1
- import{AllowlistGate as t}from"./allowlist-gate.js";import{readAllowlist as e,writeAllowlist as i}from"./allowlist-store.js";export{t as AllowlistGate,e as readAllowlist,i as writeAllowlist};
1
+ import{AllowlistGate as l}from"./allowlist-gate.js";import{readAllowlist as t,writeAllowlist as s}from"./allowlist-store.js";export{l as AllowlistGate,t as readAllowlist,s as writeAllowlist};
@@ -1 +1 @@
1
- const a="grix://card/",c="grix://open/",u="_submit",l=/\(\s*(grix:\/\/card\/[\s\S]+?)\s*\)/gi,d=/grix:\/\/card\/[^\s)\]]+/gi,m=/grix:\/\/open\/[^\s)\]]+/gi;function g(t){const n=[],r=new Set;for(const e of f(t)){const s=p(e);!s||r.has(s.normalizedUri)||(r.add(s.normalizedUri),n.push(s))}return{cardSubmissions:n}}function U(t,n){return S(t.params.get(n))}function f(t){const n=_(t),r=[];for(const s of n.matchAll(l)){const i=o(s[1]);i&&r.push(i)}for(const s of n.matchAll(d)){const i=o(s[0]);i&&r.push(i)}for(const s of n.matchAll(m)){const i=o(s[0]);i&&r.push(i)}const e=o(n.trim());return(e.startsWith(a)||e.startsWith(c))&&r.push(e),R(r)}function p(t){if(t.startsWith(c))return h(t);if(!t.startsWith(a))return null;let n;try{n=new URL(t)}catch{return null}if(n.hostname!=="card")return null;const r=n.pathname.replace(/^\/+/,"").trim();if(!r.endsWith(u))return null;const e=r.slice(0,-u.length).trim();return!e||e==="agent_open_session"?null:{cardId:e,rawUri:t,normalizedUri:n.toString(),params:n.searchParams}}function h(t){let n;try{n=new URL(t)}catch{return null}if(n.hostname!=="open")return null;const r=n.pathname.split("/").map(e=>e.trim()).filter(Boolean);return r.length!==1||r[0]!=="session"?null:{cardId:"agent_open_session",rawUri:t,normalizedUri:n.toString(),params:n.searchParams}}function _(t){return t.replace(/&amp;/gi,"&")}function o(t){return t.replace(/\s+/g,"").trim()}function R(t){const n=[...new Set(t)];return n.filter(r=>!n.some(e=>e!==r&&e.startsWith(r)))}function S(t){if(!t)return;let n=t.trim();for(let r=0;r<3;r++)try{const e=decodeURIComponent(n);if(e===n)break;n=e}catch{break}return n.trim()||void 0}export{g as parseInteractionMessage,U as readCardSubmissionParam};
1
+ const c="grix://card/",l="grix://open/",u="_submit",d=/\(\s*(grix:\/\/card\/[\s\S]+?)\s*\)/gi,m=/grix:\/\/card\/[^\s)\]]+/gi,f=/^grix:\/\/open\/\S+$/i;function g(t){const n=[],r=new Set;for(const e of p(t)){const s=h(e);!s||r.has(s.normalizedUri)||(r.add(s.normalizedUri),n.push(s))}return{cardSubmissions:n}}function C(t,n){return U(t.params.get(n))}function p(t){const n=S(t),r=[];for(const o of n.matchAll(d)){const i=a(o[1]);i&&r.push(i)}for(const o of n.matchAll(m)){const i=a(o[0]);i&&r.push(i)}const e=n.trim();f.test(e)&&r.push(e);const s=a(e);return s.startsWith(c)&&r.push(s),R(r)}function h(t){if(t.startsWith(l))return _(t);if(!t.startsWith(c))return null;let n;try{n=new URL(t)}catch{return null}if(n.hostname!=="card")return null;const r=n.pathname.replace(/^\/+/,"").trim();if(!r.endsWith(u))return null;const e=r.slice(0,-u.length).trim();return!e||e==="agent_open_session"?null:{cardId:e,rawUri:t,normalizedUri:n.toString(),params:n.searchParams}}function _(t){let n;try{n=new URL(t)}catch{return null}if(n.hostname!=="open")return null;const r=n.pathname.split("/").map(e=>e.trim()).filter(Boolean);return r.length!==1||r[0]!=="session"?null:{cardId:"agent_open_session",rawUri:t,normalizedUri:n.toString(),params:n.searchParams}}function S(t){return t.replace(/&amp;/gi,"&")}function a(t){return t.replace(/\s+/g,"").trim()}function R(t){const n=[...new Set(t)];return n.filter(r=>!n.some(e=>e!==r&&e.startsWith(r)))}function U(t){if(!t)return;let n=t.trim();for(let r=0;r<3;r++)try{const e=decodeURIComponent(n);if(e===n)break;n=e}catch{break}return n.trim()||void 0}export{g as parseInteractionMessage,C as readCardSubmissionParam};
@@ -1 +1 @@
1
- import{createServer as a}from"node:http";import{log as o}from"../log/logger.js";class v{server=null;startTime=Date.now();alive=!0;statusProvider=null;setStatusProvider(t){this.statusProvider=t}async start(t){return new Promise((s,i)=>{this.server=a((n,e)=>{if(n.url==="/healthz")if(this.alive){const r=this.statusProvider?.();e.writeHead(200,{"Content-Type":"application/json"}),e.end(JSON.stringify({status:"ok",uptime:Math.floor((Date.now()-this.startTime)/1e3),pid:process.pid,...r?{agents:r}:{}}))}else e.writeHead(503,{"Content-Type":"application/json"}),e.end(JSON.stringify({status:"shutting_down"}));else e.writeHead(404),e.end()}),this.server.listen(t,"127.0.0.1",()=>{o.info("health",`Listening on 127.0.0.1:${t}`),s()}),this.server.on("error",i)})}markShuttingDown(){this.alive=!1}async stop(){if(this.server)return new Promise(t=>{this.server.close(()=>t())})}}export{v as HealthServer};
1
+ import{createServer as o}from"node:http";import{log as a}from"../log/logger.js";import{resolveClientVersion as l}from"../util/client-version.js";class d{server=null;startTime=Date.now();alive=!0;statusProvider=null;version=l();setStatusProvider(e){this.statusProvider=e}async start(e){return new Promise((s,i)=>{this.server=o((n,t)=>{if(n.url==="/healthz")if(this.alive){const r=this.statusProvider?.();t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({status:"ok",version:this.version,uptime:Math.floor((Date.now()-this.startTime)/1e3),pid:process.pid,...r?{agents:r}:{}}))}else t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify({status:"shutting_down"}));else t.writeHead(404),t.end()}),this.server.listen(e,"127.0.0.1",()=>{a.info("health",`Listening on 127.0.0.1:${e}`),s()}),this.server.on("error",i)})}markShuttingDown(){this.alive=!1}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>e())})}}export{d as HealthServer};
@@ -0,0 +1 @@
1
+ const r=["help","version","config-dir","profile","health-port","admin-port"],c=new Map([["-h","--help"],["-v","--version"]]);function p(e){const i=[],n=Object.create(null);for(let t=0;t<e.length;t++){const s=c.get(e[t])??e[t],o=e[t+1];s.startsWith("--")?o&&!o.startsWith("-")?(n[s.slice(2)]=o,t++):n[s.slice(2)]="true":i.push(s)}const l=new Set(r);return{command:i[0],flags:n,unknownFlags:Object.keys(n).filter(t=>!l.has(t))}}export{r as KNOWN_FLAGS,p as parseCliArgs};
package/dist/grix.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import w from"node:path";import{writeFileSync as F}from"node:fs";import{Manager as N}from"./manager.js";import{ensureGrixDirs as G,initLogger as M,log as n,installProcessLogRotation as j,setConsoleOutput as W}from"./core/log/index.js";import{HealthServer as B,bindPortOrFail as T}from"./core/runtime/index.js";import{writePidFile as X,removePidFile as S,readDaemonPid as V}from"./core/runtime/index.js";import{resolveRuntimePaths as y}from"./core/config/index.js";import{ServiceManager as J}from"./service/service-manager.js";import{killProcessesByCommandLine as K,isWindowsElevated as q}from"./service/process-control.js";import{acquireDaemonLock as z,isLockHolderSameProcess as Q,readDaemonLock as Y,releaseDaemonLock as x}from"./runtime/daemon-lock.js";import{writeDaemonStatus as k,removeDaemonStatus as Z}from"./runtime/service-state.js";import{AdminServer as ee,generateToken as te,writeTokenFile as oe}from"./core/admin/index.js";import{initSentry as re,closeSentry as P,reportFatal as R}from"./core/observability/sentry.js";import{initProxyManager as ae,getProxyManager as p}from"./core/proxy/index.js";const m=process.argv.slice(2),$=[],l={};for(let t=0;t<m.length;t++)m[t].startsWith("--")&&m[t+1]&&!m[t+1].startsWith("--")?(l[m[t].slice(2)]=m[t+1],t++):m[t].startsWith("--")?l[m[t].slice(2)]="true":$.push(m[t]);l.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
2
+ import h from"node:path";import{writeFileSync as A}from"node:fs";import{Manager as _}from"./manager.js";import{ensureGrixDirs as N,initLogger as G,log as n,installProcessLogRotation as M,setConsoleOutput as j}from"./core/log/index.js";import{HealthServer as B,bindPortOrFail as $}from"./core/runtime/index.js";import{writePidFile as V,removePidFile as w,readDaemonPid as W}from"./core/runtime/index.js";import{resolveRuntimePaths as E}from"./core/config/index.js";import{ServiceManager as X}from"./service/service-manager.js";import{killProcessesByCommandLine as J,isWindowsElevated as K}from"./service/process-control.js";import{acquireDaemonLock as q,isLockHolderSameProcess as z,readDaemonLock as Q,releaseDaemonLock as S}from"./runtime/daemon-lock.js";import{writeDaemonStatus as k,removeDaemonStatus as Y}from"./runtime/service-state.js";import{AdminServer as Z,generateToken as ee,writeTokenFile as te}from"./core/admin/index.js";import{initSentry as oe,closeSentry as y,reportFatal as v}from"./core/observability/sentry.js";import{initProxyManager as re,getProxyManager as p}from"./core/proxy/index.js";import{resolveClientVersion as ae}from"./core/util/client-version.js";import{parseCliArgs as ne}from"./core/util/cli-args.js";const{command:u,flags:d,unknownFlags:P}=ne(process.argv.slice(2));if(P.length>0&&(console.error(`Unknown option${P.length>1?"s":""}: ${P.map(t=>`--${t}`).join(", ")}
3
+ Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),d.version&&(console.log(ae()),process.exit(0)),d.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
3
4
 
4
5
  Usage: grix-connector <command> [options]
5
6
 
@@ -17,7 +18,8 @@ Options:
17
18
  --profile <name> Profile name for config subdirectory
18
19
  --health-port <port> Health check port (default: 19579)
19
20
  --admin-port <port> Admin API port (default: 19580)
20
- --help Show this help message
21
+ --version, -v Print the installed version and exit
22
+ --help, -h Show this help message
21
23
 
22
24
  Platform services:
23
25
  macOS: launchd (LaunchAgent)
@@ -32,8 +34,8 @@ Examples:
32
34
  grix-connector start # Start as system service
33
35
  grix-connector status # Check service status
34
36
  grix-connector restart # Restart the service
35
- `),process.exit(0));const g=$[0];if(g==="reload"){const t=V();t||(console.error("reload failed: daemon is not running (no pid file)"),process.exit(1));try{process.kill(t,0)}catch{console.error(`reload failed: daemon process ${t} is not running (stale pid file)`),process.exit(1)}{const c=Y(y().daemonLockFile);c&&c.pid===t&&!Q(c)&&(console.error(`reload failed: pid ${t} has been reused by another process (stale pid file)`),process.exit(1))}try{process.kill(t,"SIGHUP"),console.log(JSON.stringify({ok:!0,signaled:t},null,2)),process.exit(0)}catch(c){console.error(`reload failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}const O=["start","stop","restart","status"];if(g&&O.includes(g)){process.platform==="win32"&&["start","stop","restart"].includes(g)&&!q()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
37
+ `),process.exit(0)),u==="reload"){const t=W();t||(console.error("reload failed: daemon is not running (no pid file)"),process.exit(1));try{process.kill(t,0)}catch{console.error(`reload failed: daemon process ${t} is not running (stale pid file)`),process.exit(1)}{const c=Q(E().daemonLockFile);c&&c.pid===t&&!z(c)&&(console.error(`reload failed: pid ${t} has been reused by another process (stale pid file)`),process.exit(1))}try{process.kill(t,"SIGHUP"),console.log(JSON.stringify({ok:!0,signaled:t},null,2)),process.exit(0)}catch(c){console.error(`reload failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}const T=["start","stop","restart","status"];if(u&&T.includes(u)){process.platform==="win32"&&["start","stop","restart"].includes(u)&&!K()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
36
38
  using Startup folder auto-start instead. For full Task Scheduler integration,
37
- right-click the terminal and select "Run as administrator".`);const t=y(),c=l["config-dir"]??(l.profile?w.join(t.configDir,l.profile):void 0),f=w.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),r=new J({cliPath:f,nodePath:process.execPath});try{let i;switch(g){case"start":(await r.status({rootDir:t.rootDir})).installed?i=await r.start({rootDir:t.rootDir}):i=await r.install({rootDir:t.rootDir,configDir:c});break;case"stop":i=await r.stop({rootDir:t.rootDir});break;case"restart":(await r.status({rootDir:t.rootDir})).installed?i=await r.restart({rootDir:t.rootDir}):i=await r.install({rootDir:t.rootDir,configDir:c});break;case"status":i=await r.status({rootDir:t.rootDir});break}console.log(JSON.stringify(i,null,2)),process.exit(0)}catch(i){console.error(`${g} failed: ${i instanceof Error?i.message:i}`),process.exit(1)}}else g&&(console.error(`Unknown command: ${g}
38
- Valid commands: ${O.join(", ")}`),process.exit(1));const a=y(),ne=l["config-dir"]??(l.profile?`${a.configDir}/${l.profile}`:void 0),s=new N,D=new B,L=te(),h=new ee(L);let b=!1;async function H(t){process.stderr.write(t.message+`
39
- `),n.error("main",t.message.replace(/\n/g," \u2014 ")),await k(a.daemonStatusFile,{state:"failed",pid:process.pid,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await P(),x(a.daemonLockFile).catch(()=>{}),S(),process.exit(1)}async function E(t){if(b)return;b=!0,n.info("main",`Received ${t}, shutting down...`),D.markShuttingDown();const c=setTimeout(()=>{n.error("main","Shutdown timed out, forcing exit"),x(a.daemonLockFile).catch(()=>{}),S(),process.exit(2)},1e4);try{await s.stop(),await p()?.stop().catch(()=>{}),await h.stop(),await D.stop(),await P(),await x(a.daemonLockFile),await Z(a.daemonStatusFile).catch(()=>{}),clearTimeout(c),S(),n.info("main","Shutdown complete"),process.exit(0)}catch(f){n.error("main",`Shutdown error: ${f}`),x(a.daemonLockFile).catch(()=>{}),S(),process.exit(2)}}async function se(){G(),M(),await re(),j(a.stdoutLogFile,a.stderrLogFile),W(!1),process.platform==="win32"&&await K("GrixConnectorDaemon",{platform:"win32"});try{await z(a.daemonLockFile,a.rootDir)}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}X(),n.info("main",`grix-connector starting (PID ${process.pid})`),await k(a.daemonStatusFile,{state:"starting",pid:process.pid,updated_at:Date.now()});const t=parseInt(l["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await T({label:"health",port:t,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:o=>D.start(o)});e&&await H(e)}const c=w.join(a.dataDir,"health-port");F(c,String(t),"utf-8"),process.on("SIGINT",()=>E("SIGINT")),process.on("SIGTERM",()=>E("SIGTERM")),process.on("SIGHUP",()=>{b||(n.info("main","Received SIGHUP, reloading config..."),s.reload().then(e=>n.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>n.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let f="",r=0,i;process.on("uncaughtException",e=>{const o=e instanceof Error?e.stack??e.message:String(e);o===f?(r++,(r<=3||r%100===0)&&n.error("main",`Uncaught exception (x${r}): ${o}`)):(r>3&&n.error("main",`Previous exception repeated ${r} times total`),f=o,r=1,n.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),i||(i=setTimeout(()=>{r>3&&n.error("main",`Previous exception repeated ${r} times total`),f="",r=0,i=void 0},1e4).unref())),!C(e)&&(R(e,"uncaughtException"),E("uncaughtException"))}),process.on("unhandledRejection",e=>{n.error("main",`Unhandled rejection: ${e}`),!C(e)&&(R(e,"unhandledRejection"),E("unhandledRejection"))}),await ae(a.dataDir).clearRuntimeState(),n.info("main",'mitm proxy disabled by default (enable online: PUT /api/proxy/enabled {"enabled":true}; not persisted, resets to off on restart)'),D.setStatusProvider(()=>s.getAgentsStatus()),await s.start(ne);const I=parseInt(l["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);if(h.setAgentHandler({list:()=>s.getAgentsStatus(),add:e=>s.addAgent(e),remove:e=>s.removeAgent(e),restart:e=>s.restartAgent(e),reload:()=>s.reload()}),h.setUpgradeHandler({check:()=>s.checkUpgrade(),trigger:()=>s.triggerUpgrade()}),h.setProbeHandler({probeAll:e=>s.probeAll(e),probeOne:(e,o)=>s.probeOne(e,o)}),h.setInstallHandler({listInstallable:()=>s.listInstallable(),installAgent:e=>s.installAgent(e),getInstallProgress:e=>s.getInstallProgress(e)}),p()){const e=()=>{const o=p();return{enabled:o.isInjectionEnabled(),runtime:o.getRuntimeInfo(),config:o.getConfigSnapshot()}};h.setProxyHandler({status:()=>e(),enable:async()=>(await p().enable(),e()),disable:async()=>(await p().disable(),e()),setRoute:async(o,d)=>{const u=d,A=p();return A.setRoute({routeKey:o,targetBaseUrl:u.targetBaseUrl,...u.headers?{headers:u.headers}:{},...u.model?{model:u.model}:{},...u.modelMap?{modelMap:u.modelMap}:{},...u.passthrough?{passthrough:!0}:{}}),await A.persistConfig(),e()},deleteRoute:async o=>{const d=p();d.deleteRoute(o),await d.persistConfig()},setDefaultRoute:async o=>{const d=p();return d.setDefaultRouteKey(o),await d.persistConfig(),e()},setInterceptHosts:async o=>{const d=p();return d.setInterceptHosts(o),await d.persistConfig(),e()}})}{const e=await T({label:"admin",port:I,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:o=>h.start(o)});e&&await H(e)}const U=w.join(a.dataDir,"admin-token"),_=w.join(a.dataDir,"admin-port");oe(U,L),F(_,String(I),"utf-8"),await k(a.daemonStatusFile,{state:"running",pid:process.pid,updated_at:Date.now()}),process.send&&process.send("ready"),n.info("main","grix-connector ready")}se().catch(async t=>{n.error("main",`Fatal: ${t}`),R(t,"startup"),await P(),x(a.daemonLockFile).catch(()=>{}),S(),process.exit(1)});const ie=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function C(t){return t instanceof Error&&"code"in t?ie.has(t.code):!1}
39
+ right-click the terminal and select "Run as administrator".`);const t=E(),c=d["config-dir"]??(d.profile?h.join(t.configDir,d.profile):void 0),g=h.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),r=new X({cliPath:g,nodePath:process.execPath});try{let s;switch(u){case"start":(await r.status({rootDir:t.rootDir})).installed?s=await r.start({rootDir:t.rootDir}):s=await r.install({rootDir:t.rootDir,configDir:c});break;case"stop":s=await r.stop({rootDir:t.rootDir});break;case"restart":(await r.status({rootDir:t.rootDir})).installed?s=await r.restart({rootDir:t.rootDir}):s=await r.install({rootDir:t.rootDir,configDir:c});break;case"status":s=await r.status({rootDir:t.rootDir});break}console.log(JSON.stringify(s,null,2)),process.exit(0)}catch(s){console.error(`${u} failed: ${s instanceof Error?s.message:s}`),process.exit(1)}}else u&&(console.error(`Unknown command: ${u}
40
+ Valid commands: ${T.join(", ")}`),process.exit(1));const a=E(),ie=d["config-dir"]??(d.profile?`${a.configDir}/${d.profile}`:void 0),i=new _,x=new B,O=ee(),f=new Z(O);let R=!1;async function C(t){process.stderr.write(t.message+`
41
+ `),n.error("main",t.message.replace(/\n/g," \u2014 ")),await k(a.daemonStatusFile,{state:"failed",pid:process.pid,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await y(),S(a.daemonLockFile).catch(()=>{}),w(),process.exit(1)}async function D(t){if(R)return;R=!0,n.info("main",`Received ${t}, shutting down...`),x.markShuttingDown();const c=setTimeout(()=>{n.error("main","Shutdown timed out, forcing exit"),S(a.daemonLockFile).catch(()=>{}),w(),process.exit(2)},1e4);try{await i.stop(),await p()?.stop().catch(()=>{}),await f.stop(),await x.stop(),await y(),await S(a.daemonLockFile),await Y(a.daemonStatusFile).catch(()=>{}),clearTimeout(c),w(),n.info("main","Shutdown complete"),process.exit(0)}catch(g){n.error("main",`Shutdown error: ${g}`),S(a.daemonLockFile).catch(()=>{}),w(),process.exit(2)}}async function se(){N(),G(),await oe(),M(a.stdoutLogFile,a.stderrLogFile),j(!1),process.platform==="win32"&&await J("GrixConnectorDaemon",{platform:"win32"});try{await q(a.daemonLockFile,a.rootDir)}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}V(),n.info("main",`grix-connector starting (PID ${process.pid})`),await k(a.daemonStatusFile,{state:"starting",pid:process.pid,updated_at:Date.now()});const t=parseInt(d["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await $({label:"health",port:t,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:o=>x.start(o)});e&&await C(e)}const c=h.join(a.dataDir,"health-port");A(c,String(t),"utf-8"),process.on("SIGINT",()=>D("SIGINT")),process.on("SIGTERM",()=>D("SIGTERM")),process.on("SIGHUP",()=>{R||(n.info("main","Received SIGHUP, reloading config..."),i.reload().then(e=>n.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>n.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let g="",r=0,s;process.on("uncaughtException",e=>{const o=e instanceof Error?e.stack??e.message:String(e);o===g?(r++,(r<=3||r%100===0)&&n.error("main",`Uncaught exception (x${r}): ${o}`)):(r>3&&n.error("main",`Previous exception repeated ${r} times total`),g=o,r=1,n.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),s||(s=setTimeout(()=>{r>3&&n.error("main",`Previous exception repeated ${r} times total`),g="",r=0,s=void 0},1e4).unref())),!L(e)&&(v(e,"uncaughtException"),D("uncaughtException"))}),process.on("unhandledRejection",e=>{n.error("main",`Unhandled rejection: ${e}`),!L(e)&&(v(e,"unhandledRejection"),D("unhandledRejection"))}),await re(a.dataDir).clearRuntimeState(),n.info("main",'mitm proxy disabled by default (enable online: PUT /api/proxy/enabled {"enabled":true}; not persisted, resets to off on restart)'),x.setStatusProvider(()=>i.getAgentsStatus()),await i.start(ie);const I=parseInt(d["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);if(f.setAgentHandler({list:()=>i.getAgentsStatus(),add:e=>i.addAgent(e),remove:e=>i.removeAgent(e),restart:e=>i.restartAgent(e),reload:()=>i.reload()}),f.setUpgradeHandler({check:()=>i.checkUpgrade(),trigger:()=>i.triggerUpgrade()}),f.setProbeHandler({probeAll:e=>i.probeAll(e),probeOne:(e,o)=>i.probeOne(e,o)}),f.setInstallHandler({listInstallable:()=>i.listInstallable(),installAgent:e=>i.installAgent(e),getInstallProgress:e=>i.getInstallProgress(e)}),p()){const e=()=>{const o=p();return{enabled:o.isInjectionEnabled(),runtime:o.getRuntimeInfo(),config:o.getConfigSnapshot()}};f.setProxyHandler({status:()=>e(),enable:async()=>(await p().enable(),e()),disable:async()=>(await p().disable(),e()),setRoute:async(o,l)=>{const m=l,F=p();return F.setRoute({routeKey:o,targetBaseUrl:m.targetBaseUrl,...m.headers?{headers:m.headers}:{},...m.model?{model:m.model}:{},...m.modelMap?{modelMap:m.modelMap}:{},...m.passthrough?{passthrough:!0}:{}}),await F.persistConfig(),e()},deleteRoute:async o=>{const l=p();l.deleteRoute(o),await l.persistConfig()},setDefaultRoute:async o=>{const l=p();return l.setDefaultRouteKey(o),await l.persistConfig(),e()},setInterceptHosts:async o=>{const l=p();return l.setInterceptHosts(o),await l.persistConfig(),e()}})}{const e=await $({label:"admin",port:I,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:o=>f.start(o)});e&&await C(e)}const H=h.join(a.dataDir,"admin-token"),U=h.join(a.dataDir,"admin-port");te(H,O),A(U,String(I),"utf-8"),await k(a.daemonStatusFile,{state:"running",pid:process.pid,updated_at:Date.now()}),process.send&&process.send("ready"),n.info("main","grix-connector ready")}se().catch(async t=>{n.error("main",`Fatal: ${t}`),v(t,"startup"),await y(),S(a.daemonLockFile).catch(()=>{}),w(),process.exit(1)});const ce=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function L(t){return t instanceof Error&&"code"in t?ce.has(t.code):!1}
@@ -1 +1 @@
1
- function a(t){const o=new Set([`http://127.0.0.1:${t.serverPort}`,`http://localhost:${t.serverPort}`,...t.allowedOrigins]),e=new Set([`127.0.0.1:${t.serverPort}`,`localhost:${t.serverPort}`,...t.allowedHosts]);return{validateRequest(s){const r=i(s,o);if(!r.ok)return r;const n=l(s,e);return n.ok?{ok:!0}:n}}}function i(t,o){const e=t.headers.origin;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${e}`}:{ok:!0}}function l(t,o){const e=t.headers.host;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${e}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
1
+ function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "3.3.2",
3
+ "version": "3.3.5",
4
4
  "description": "Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",