grix-connector 3.1.13 → 3.1.15

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 (75) hide show
  1. package/README.md +241 -241
  2. package/dist/adapter/claude/claude-adapter.js +19 -19
  3. package/dist/adapter/claude/usage-parser.js +9 -7
  4. package/dist/default-skills/grix-access-control/SKILL.md +31 -31
  5. package/dist/default-skills/grix-admin/SKILL.md +35 -35
  6. package/dist/default-skills/grix-agent-dispatch/SKILL.md +89 -89
  7. package/dist/default-skills/grix-chat-state/SKILL.md +56 -56
  8. package/dist/default-skills/grix-egg/SKILL.md +90 -90
  9. package/dist/default-skills/grix-group/SKILL.md +35 -35
  10. package/dist/default-skills/grix-owner-relay/SKILL.md +66 -66
  11. package/dist/default-skills/grix-query/SKILL.md +38 -38
  12. package/dist/default-skills/message-send/SKILL.md +36 -36
  13. package/dist/default-skills/message-unsend/SKILL.md +27 -27
  14. package/dist/default-skills/tailnet-file-share/SKILL.md +65 -65
  15. package/dist/grix.js +0 -0
  16. package/dist/service/platform-adapter.js +59 -16
  17. package/openclaw-plugin/skills/grix-admin/SKILL.md +202 -202
  18. package/openclaw-plugin/skills/grix-admin/references/api-contract.md +210 -210
  19. package/openclaw-plugin/skills/grix-egg/SKILL.md +81 -81
  20. package/openclaw-plugin/skills/grix-egg/references/api-contract.md +40 -40
  21. package/openclaw-plugin/skills/grix-group/SKILL.md +164 -164
  22. package/openclaw-plugin/skills/grix-group/references/api-contract.md +97 -97
  23. package/openclaw-plugin/skills/grix-query/SKILL.md +247 -247
  24. package/openclaw-plugin/skills/grix-register/SKILL.md +86 -86
  25. package/openclaw-plugin/skills/grix-register/references/api-contract.md +76 -76
  26. package/openclaw-plugin/skills/grix-register/references/grix-concepts.md +26 -26
  27. package/openclaw-plugin/skills/grix-register/references/handoff-contract.md +24 -24
  28. package/openclaw-plugin/skills/grix-register/references/openclaw-setup.md +6 -6
  29. package/openclaw-plugin/skills/grix-register/references/user-replies.md +25 -25
  30. package/openclaw-plugin/skills/grix-update/SKILL.md +310 -310
  31. package/openclaw-plugin/skills/grix-update/references/cron-setup.md +56 -56
  32. package/openclaw-plugin/skills/grix-update/references/update-contract.md +149 -149
  33. package/openclaw-plugin/skills/message-send/SKILL.md +197 -197
  34. package/openclaw-plugin/skills/message-unsend/SKILL.md +186 -186
  35. package/openclaw-plugin/skills/message-unsend/flowchart.mermaid +27 -27
  36. package/openclaw-plugin/skills/openclaw-memory-setup/SKILL.md +282 -282
  37. package/openclaw-plugin/skills/openclaw-memory-setup/references/case-study-macpro.md +52 -52
  38. package/openclaw-plugin/skills/openclaw-memory-setup/references/host-readiness.md +147 -147
  39. package/openclaw.plugin.json +24 -24
  40. package/package.json +121 -121
  41. package/scripts/install-guardian.mjs +27 -27
  42. package/scripts/install-guardian.sh +25 -25
  43. package/scripts/upgrade-guardian.sh +104 -104
  44. package/dist/adapter/claude/claude-bridge-server.js +0 -1
  45. package/dist/adapter/claude/claude-tools.js +0 -1
  46. package/dist/adapter/claude/claude-worker-client.js +0 -1
  47. package/dist/adapter/claude/mcp-http-launcher.js +0 -2
  48. package/dist/adapter/claude/result-timeout.js +0 -1
  49. package/dist/adapter/deepseek/deepseek-adapter.js +0 -6
  50. package/dist/adapter/deepseek/index.js +0 -1
  51. package/dist/adapter/qwen/index.js +0 -1
  52. package/dist/adapter/qwen/qwen-adapter.js +0 -4
  53. package/dist/aibot/client.js +0 -1
  54. package/dist/aibot/index.js +0 -1
  55. package/dist/aibot/types.js +0 -0
  56. package/dist/core/file-ops/handler.js +0 -1
  57. package/dist/core/file-ops/list-files.js +0 -1
  58. package/dist/core/file-ops/types.js +0 -0
  59. package/dist/default-skills/grix-task-status/SKILL.md +0 -36
  60. package/dist/log.js +0 -3
  61. package/dist/main.js +0 -31
  62. package/dist/mcp/stream-http/config.js +0 -1
  63. package/dist/mcp/stream-http/connection-binding.js +0 -1
  64. package/dist/mcp/stream-http/event-tool-executor.js +0 -1
  65. package/dist/mcp/stream-http/gateway.js +0 -1
  66. package/dist/mcp/stream-http/index.js +0 -1
  67. package/dist/mcp/stream-http/security.js +0 -1
  68. package/dist/mcp/stream-http/session-manager.js +0 -1
  69. package/dist/mcp/stream-http/tool-executor.js +0 -1
  70. package/dist/mcp/stream-http/tool-registry.js +0 -1
  71. package/dist/mcp/stream-http/tool-schemas.js +0 -1
  72. package/dist/session/index.js +0 -1
  73. package/dist/session/manager.js +0 -1
  74. package/dist/transport/index.js +0 -1
  75. package/dist/transport/json-rpc.js +0 -3
package/package.json CHANGED
@@ -1,121 +1,121 @@
1
- {
2
- "name": "grix-connector",
3
- "version": "3.1.13",
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
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "grix-connector": "dist/grix.js"
9
- },
10
- "files": [
11
- "dist",
12
- "!dist/**/*.map",
13
- "!dist/**/*.d.ts",
14
- "!dist/**/*.ts",
15
- "openclaw-plugin",
16
- "scripts/install-guardian.mjs",
17
- "scripts/install-guardian.sh",
18
- "scripts/upgrade-guardian.sh",
19
- "openclaw.plugin.json"
20
- ],
21
- "scripts": {
22
- "postinstall": "node scripts/install-guardian.mjs",
23
- "prepublishOnly": "npm run build:all",
24
- "build": "npm run check:isolation && tsc && node -e \"require('fs').cpSync('src/default-skills','dist/default-skills',{recursive:true,filter:s=>!s.endsWith('.ts')})\"",
25
- "check:isolation": "node scripts/check-adapter-isolation.mjs",
26
- "build:plugin": "node scripts/build-plugin.mjs",
27
- "build:all": "npm run build && node scripts/minify-dist.mjs && npm run build:plugin",
28
- "dev": "npm run build && node dist/grix.js",
29
- "test": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run",
30
- "test:watch": "node --experimental-vm-modules node_modules/vitest/vitest.mjs",
31
- "test:e2e": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e",
32
- "test:e2e:smoke": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/smoke.test.ts",
33
- "lint": "npm run check:isolation && tsc --noEmit",
34
- "start": "node dist/grix.js start",
35
- "stop": "node dist/grix.js stop",
36
- "restart": "node dist/grix.js restart",
37
- "status": "node dist/grix.js status"
38
- },
39
- "keywords": [
40
- "grix",
41
- "aibot",
42
- "agent-connector",
43
- "claude",
44
- "codex",
45
- "gemini",
46
- "qwen",
47
- "deepseek",
48
- "cursor",
49
- "opencode",
50
- "pi",
51
- "openhuman",
52
- "reasonix",
53
- "acp",
54
- "agent-client-protocol",
55
- "mcp",
56
- "openclaw",
57
- "openclaw-plugin"
58
- ],
59
- "author": "askie",
60
- "repository": {
61
- "type": "git",
62
- "url": "git+https://github.com/askie/grix-connector.git"
63
- },
64
- "homepage": "https://github.com/askie/grix-connector#readme",
65
- "bugs": {
66
- "url": "https://github.com/askie/grix-connector/issues"
67
- },
68
- "license": "MIT",
69
- "devDependencies": {
70
- "@types/node": "^25.6.0",
71
- "@types/node-forge": "^1.3.14",
72
- "@types/ws": "^8.18.1",
73
- "esbuild": "^0.27.7",
74
- "fast-check": "^4.8.0",
75
- "typescript": "^5.7.0",
76
- "vitest": "^3.0.0"
77
- },
78
- "peerDependencies": {
79
- "openclaw": ">=2026.4.8"
80
- },
81
- "peerDependenciesMeta": {
82
- "openclaw": {
83
- "optional": true
84
- }
85
- },
86
- "engines": {
87
- "node": ">=18.0.0"
88
- },
89
- "dependencies": {
90
- "@modelcontextprotocol/sdk": "^1.29.0",
91
- "@sentry/node": "^10.57.0",
92
- "extract-zip": "^2.0.1",
93
- "node-forge": "^1.4.0",
94
- "socket.io-client": "^4.8.3",
95
- "ws": "^8.20.0"
96
- },
97
- "optionalDependencies": {
98
- "node-pty": "^1.1.0"
99
- },
100
- "openclaw": {
101
- "extensions": [
102
- "./openclaw-plugin/index.js"
103
- ],
104
- "channel": {
105
- "id": "grix",
106
- "label": "Grix",
107
- "selectionLabel": "Grix",
108
- "docsPath": "/channels/grix",
109
- "blurb": "Connect OpenClaw to a Grix deployment for website management with mobile PWA support.",
110
- "aliases": [
111
- "gr"
112
- ],
113
- "order": 90
114
- },
115
- "install": {
116
- "npmSpec": "grix-connector",
117
- "localPath": ".",
118
- "defaultChoice": "npm"
119
- }
120
- }
121
- }
1
+ {
2
+ "name": "grix-connector",
3
+ "version": "3.1.15",
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
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "grix-connector": "dist/grix.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map",
13
+ "!dist/**/*.d.ts",
14
+ "!dist/**/*.ts",
15
+ "openclaw-plugin",
16
+ "scripts/install-guardian.mjs",
17
+ "scripts/install-guardian.sh",
18
+ "scripts/upgrade-guardian.sh",
19
+ "openclaw.plugin.json"
20
+ ],
21
+ "scripts": {
22
+ "postinstall": "node scripts/install-guardian.mjs",
23
+ "prepublishOnly": "npm run build:all",
24
+ "build": "npm run check:isolation && tsc && node -e \"require('fs').cpSync('src/default-skills','dist/default-skills',{recursive:true,filter:s=>!s.endsWith('.ts')})\"",
25
+ "check:isolation": "node scripts/check-adapter-isolation.mjs",
26
+ "build:plugin": "node scripts/build-plugin.mjs",
27
+ "build:all": "npm run build && node scripts/minify-dist.mjs && npm run build:plugin",
28
+ "dev": "npm run build && node dist/grix.js",
29
+ "test": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run",
30
+ "test:watch": "node --experimental-vm-modules node_modules/vitest/vitest.mjs",
31
+ "test:e2e": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e",
32
+ "test:e2e:smoke": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/smoke.test.ts",
33
+ "lint": "npm run check:isolation && tsc --noEmit",
34
+ "start": "node dist/grix.js start",
35
+ "stop": "node dist/grix.js stop",
36
+ "restart": "node dist/grix.js restart",
37
+ "status": "node dist/grix.js status"
38
+ },
39
+ "keywords": [
40
+ "grix",
41
+ "aibot",
42
+ "agent-connector",
43
+ "claude",
44
+ "codex",
45
+ "gemini",
46
+ "qwen",
47
+ "deepseek",
48
+ "cursor",
49
+ "opencode",
50
+ "pi",
51
+ "openhuman",
52
+ "reasonix",
53
+ "acp",
54
+ "agent-client-protocol",
55
+ "mcp",
56
+ "openclaw",
57
+ "openclaw-plugin"
58
+ ],
59
+ "author": "askie",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/askie/grix-connector.git"
63
+ },
64
+ "homepage": "https://github.com/askie/grix-connector#readme",
65
+ "bugs": {
66
+ "url": "https://github.com/askie/grix-connector/issues"
67
+ },
68
+ "license": "MIT",
69
+ "devDependencies": {
70
+ "@types/node": "^25.6.0",
71
+ "@types/node-forge": "^1.3.14",
72
+ "@types/ws": "^8.18.1",
73
+ "esbuild": "^0.27.7",
74
+ "fast-check": "^4.8.0",
75
+ "typescript": "^5.7.0",
76
+ "vitest": "^3.0.0"
77
+ },
78
+ "peerDependencies": {
79
+ "openclaw": ">=2026.4.8"
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "openclaw": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "engines": {
87
+ "node": ">=18.0.0"
88
+ },
89
+ "dependencies": {
90
+ "@modelcontextprotocol/sdk": "^1.29.0",
91
+ "@sentry/node": "^10.57.0",
92
+ "extract-zip": "^2.0.1",
93
+ "node-forge": "^1.4.0",
94
+ "socket.io-client": "^4.8.3",
95
+ "ws": "^8.20.0"
96
+ },
97
+ "optionalDependencies": {
98
+ "node-pty": "^1.1.0"
99
+ },
100
+ "openclaw": {
101
+ "extensions": [
102
+ "./openclaw-plugin/index.js"
103
+ ],
104
+ "channel": {
105
+ "id": "grix",
106
+ "label": "Grix",
107
+ "selectionLabel": "Grix",
108
+ "docsPath": "/channels/grix",
109
+ "blurb": "Connect OpenClaw to a Grix deployment for website management with mobile PWA support.",
110
+ "aliases": [
111
+ "gr"
112
+ ],
113
+ "order": 90
114
+ },
115
+ "install": {
116
+ "npmSpec": "grix-connector",
117
+ "localPath": ".",
118
+ "defaultChoice": "npm"
119
+ }
120
+ }
121
+ }
@@ -1,27 +1,27 @@
1
- #!/usr/bin/env node
2
- // install-guardian — cross-platform npm postinstall script
3
- // 每次 npm install 都把 upgrade-guardian.sh 强制覆盖到 ~/.grix/bin/,
4
- // 保证老用户也能拿到新版 guardian(不再以 "用户可能自改" 为由跳过覆盖)。
5
- // On Windows, skips since the guardian is a bash script.
6
-
7
- import { existsSync, copyFileSync, chmodSync, mkdirSync } from 'node:fs';
8
- import { join, resolve } from 'node:path';
9
- import { homedir } from 'node:os';
10
-
11
- if (process.platform === 'win32') {
12
- process.exit(0);
13
- }
14
-
15
- const GRIX_HOME = process.env.GRIX_CONNECTOR_HOME || join(homedir(), '.grix');
16
- const GUARDIAN_DEST = join(GRIX_HOME, 'bin', 'upgrade-guardian.sh');
17
-
18
- const INIT_CWD = process.env.INIT_CWD || resolve(import.meta.dirname, '..');
19
- const GUARDIAN_SRC = join(INIT_CWD, 'scripts', 'upgrade-guardian.sh');
20
-
21
- if (!existsSync(GUARDIAN_SRC)) {
22
- process.exit(0);
23
- }
24
-
25
- mkdirSync(join(GRIX_HOME, 'bin'), { recursive: true });
26
- copyFileSync(GUARDIAN_SRC, GUARDIAN_DEST);
27
- try { chmodSync(GUARDIAN_DEST, 0o755); } catch { /* best effort */ }
1
+ #!/usr/bin/env node
2
+ // install-guardian — cross-platform npm postinstall script
3
+ // 每次 npm install 都把 upgrade-guardian.sh 强制覆盖到 ~/.grix/bin/,
4
+ // 保证老用户也能拿到新版 guardian(不再以 "用户可能自改" 为由跳过覆盖)。
5
+ // On Windows, skips since the guardian is a bash script.
6
+
7
+ import { existsSync, copyFileSync, chmodSync, mkdirSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+
11
+ if (process.platform === 'win32') {
12
+ process.exit(0);
13
+ }
14
+
15
+ const GRIX_HOME = process.env.GRIX_CONNECTOR_HOME || join(homedir(), '.grix');
16
+ const GUARDIAN_DEST = join(GRIX_HOME, 'bin', 'upgrade-guardian.sh');
17
+
18
+ const INIT_CWD = process.env.INIT_CWD || resolve(import.meta.dirname, '..');
19
+ const GUARDIAN_SRC = join(INIT_CWD, 'scripts', 'upgrade-guardian.sh');
20
+
21
+ if (!existsSync(GUARDIAN_SRC)) {
22
+ process.exit(0);
23
+ }
24
+
25
+ mkdirSync(join(GRIX_HOME, 'bin'), { recursive: true });
26
+ copyFileSync(GUARDIAN_SRC, GUARDIAN_DEST);
27
+ try { chmodSync(GUARDIAN_DEST, 0o755); } catch { /* best effort */ }
@@ -1,25 +1,25 @@
1
- #!/bin/bash
2
- # install-guardian.sh — npm postinstall script
3
- # 每次 npm install 都把 upgrade-guardian.sh 强制覆盖到 ~/.grix/bin/,
4
- # 保证老用户也能拿到新版 guardian(不再以 "用户可能自改" 为由跳过覆盖)。
5
-
6
- set -e
7
-
8
- GRIX_HOME="${GRIX_CONNECTOR_HOME:-$HOME/.grix}"
9
- GUARDIAN_DEST="$GRIX_HOME/bin/upgrade-guardian.sh"
10
-
11
- # Locate source: INIT_CWD is set by npm to the package root during lifecycle scripts
12
- GUARDIAN_SRC="${INIT_CWD:-$(dirname "$0")}/scripts/upgrade-guardian.sh"
13
- if [ ! -f "$GUARDIAN_SRC" ]; then
14
- # Fallback: try relative to this script
15
- GUARDIAN_SRC="$(cd "$(dirname "$0")" && pwd)/upgrade-guardian.sh"
16
- fi
17
-
18
- if [ ! -f "$GUARDIAN_SRC" ]; then
19
- echo "grix-connector postinstall: guardian source not found, skipping" >&2
20
- exit 0
21
- fi
22
-
23
- mkdir -p "$GRIX_HOME/bin"
24
- cp "$GUARDIAN_SRC" "$GUARDIAN_DEST"
25
- chmod +x "$GUARDIAN_DEST"
1
+ #!/bin/bash
2
+ # install-guardian.sh — npm postinstall script
3
+ # 每次 npm install 都把 upgrade-guardian.sh 强制覆盖到 ~/.grix/bin/,
4
+ # 保证老用户也能拿到新版 guardian(不再以 "用户可能自改" 为由跳过覆盖)。
5
+
6
+ set -e
7
+
8
+ GRIX_HOME="${GRIX_CONNECTOR_HOME:-$HOME/.grix}"
9
+ GUARDIAN_DEST="$GRIX_HOME/bin/upgrade-guardian.sh"
10
+
11
+ # Locate source: INIT_CWD is set by npm to the package root during lifecycle scripts
12
+ GUARDIAN_SRC="${INIT_CWD:-$(dirname "$0")}/scripts/upgrade-guardian.sh"
13
+ if [ ! -f "$GUARDIAN_SRC" ]; then
14
+ # Fallback: try relative to this script
15
+ GUARDIAN_SRC="$(cd "$(dirname "$0")" && pwd)/upgrade-guardian.sh"
16
+ fi
17
+
18
+ if [ ! -f "$GUARDIAN_SRC" ]; then
19
+ echo "grix-connector postinstall: guardian source not found, skipping" >&2
20
+ exit 0
21
+ fi
22
+
23
+ mkdir -p "$GRIX_HOME/bin"
24
+ cp "$GUARDIAN_SRC" "$GUARDIAN_DEST"
25
+ chmod +x "$GUARDIAN_DEST"
@@ -1,104 +1,104 @@
1
- #!/bin/bash
2
- # upgrade-guardian.sh — post-upgrade health monitor
3
- # Started by daemon after npm install, runs as detached background process.
4
- # Monitors healthz for 90s, rolls back on crash_count >= 3.
5
- # This file is NOT inside the npm package — installed once to ~/.grix/bin/.
6
-
7
- GRIX_HOME="${GRIX_CONNECTOR_HOME:-$HOME/.grix}"
8
- PENDING_FILE="$GRIX_HOME/data/upgrade-pending.json"
9
- LOCK_FILE="$GRIX_HOME/upgrade-guardian.lock"
10
- LOG_FILE="$GRIX_HOME/log/upgrade.log"
11
- MAX_CRASHES=3
12
- HEALTH_TIMEOUT=90
13
- HEALTH_PORT_FILE="$GRIX_HOME/data/health-port"
14
- HEALTH_PORT=$(cat "$HEALTH_PORT_FILE" 2>/dev/null || echo "19579")
15
- HEALTH_URL="http://127.0.0.1:${HEALTH_PORT}/healthz"
16
- NPM_PACKAGE="grix-connector"
17
-
18
- # Ensure directories exist
19
- mkdir -p "$GRIX_HOME/log" "$GRIX_HOME/data"
20
-
21
- log() {
22
- echo "[$(date '+%Y-%m-%dT%H:%M:%S%z')] guardian: $1" >> "$LOG_FILE"
23
- }
24
-
25
- # Prevent duplicate guardian instances
26
- if [ -f "$LOCK_FILE" ]; then
27
- LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
28
- if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
29
- exit 0
30
- fi
31
- rm -f "$LOCK_FILE"
32
- fi
33
- echo $$ > "$LOCK_FILE"
34
- trap 'rm -f "$LOCK_FILE"' EXIT
35
-
36
- # Read pending marker
37
- if [ ! -f "$PENDING_FILE" ]; then
38
- exit 0
39
- fi
40
-
41
- # Extract fields from JSON (portable: no jq dependency)
42
- FROM_VERSION=$(cat "$PENDING_FILE" | grep -o '"from_version":"[^"]*"' | cut -d'"' -f4)
43
- TARGET_VERSION=$(cat "$PENDING_FILE" | grep -o '"target_version":"[^"]*"' | cut -d'"' -f4)
44
- UPGRADED_AT=$(cat "$PENDING_FILE" | grep -o '"upgraded_at":"[^"]*"' | cut -d'"' -f4)
45
- CRASH_COUNT=$(cat "$PENDING_FILE" | grep -o '"crash_count":[0-9]*' | cut -d: -f2)
46
- CRASH_COUNT=${CRASH_COUNT:-0}
47
-
48
- # Validate pending file
49
- if [ -z "$FROM_VERSION" ]; then
50
- log "pending file corrupt, missing from_version, removing"
51
- rm -f "$PENDING_FILE"
52
- exit 0
53
- fi
54
-
55
- log "started: from=$FROM_VERSION target=$TARGET_VERSION crash_count=$CRASH_COUNT"
56
-
57
- # Wait for old daemon to exit (SIGTERM + shutdown + restart)
58
- sleep 15
59
-
60
- # Monitor loop: wait for healthz to return 200
61
- ELAPSED=0
62
- while [ $ELAPSED -lt $HEALTH_TIMEOUT ]; do
63
- if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
64
- # healthz 通过=新版健康。这里不删 pending:删了会抢在新进程的
65
- # handlePendingOnStartup 之前,使其读不到 pending、success 回执发不出去
66
- # (后端只见 installed 不见 success)。交由新进程上报 success 后自行删 pending,
67
- # guardian 只负责崩溃回滚。
68
- log "healthz passed, upgrade healthy; leaving pending for in-process success report"
69
- exit 0
70
- fi
71
- sleep 3
72
- ELAPSED=$((ELAPSED + 3))
73
- done
74
-
75
- # Extra grace period for slow systems
76
- sleep 15
77
- if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
78
- # 同上:不删 pending,交由进程内 handlePendingOnStartup 上报 success 并删除。
79
- log "healthz passed (delayed), upgrade healthy; leaving pending for in-process success report"
80
- exit 0
81
- fi
82
-
83
- # Health check failed
84
- CRASH_COUNT=$((CRASH_COUNT + 1))
85
- log "healthz timeout, crash_count=$CRASH_COUNT"
86
-
87
- if [ $CRASH_COUNT -ge $MAX_CRASHES ]; then
88
- log "rollback: installing ${NPM_PACKAGE}@${FROM_VERSION}"
89
- # P5: 先用默认源,失败再显式回退官方 npm(默认源可能是淘宝镜像,旧版若没同步同样会失败)。
90
- if npm install -g "${NPM_PACKAGE}@${FROM_VERSION}" --prefer-online --no-audit --no-fund >> "$LOG_FILE" 2>&1 \
91
- || { log "rollback via default registry failed, retrying official npm"; \
92
- npm install -g "${NPM_PACKAGE}@${FROM_VERSION}" --registry https://registry.npmjs.org --prefer-online --no-audit --no-fund >> "$LOG_FILE" 2>&1; }; then
93
- log "rollback succeeded"
94
- rm -f "$PENDING_FILE"
95
- else
96
- log "ROLLBACK FAILED — manual intervention required"
97
- fi
98
- exit 0
99
- fi
100
-
101
- # Under threshold — update crash_count and exit, let process manager restart
102
- echo "{\"from_version\":\"$FROM_VERSION\",\"target_version\":\"$TARGET_VERSION\",\"upgraded_at\":\"$UPGRADED_AT\",\"crash_count\":$CRASH_COUNT}" > "$PENDING_FILE.tmp"
103
- mv "$PENDING_FILE.tmp" "$PENDING_FILE"
104
- log "updated crash_count to $CRASH_COUNT, exiting"
1
+ #!/bin/bash
2
+ # upgrade-guardian.sh — post-upgrade health monitor
3
+ # Started by daemon after npm install, runs as detached background process.
4
+ # Monitors healthz for 90s, rolls back on crash_count >= 3.
5
+ # This file is NOT inside the npm package — installed once to ~/.grix/bin/.
6
+
7
+ GRIX_HOME="${GRIX_CONNECTOR_HOME:-$HOME/.grix}"
8
+ PENDING_FILE="$GRIX_HOME/data/upgrade-pending.json"
9
+ LOCK_FILE="$GRIX_HOME/upgrade-guardian.lock"
10
+ LOG_FILE="$GRIX_HOME/log/upgrade.log"
11
+ MAX_CRASHES=3
12
+ HEALTH_TIMEOUT=90
13
+ HEALTH_PORT_FILE="$GRIX_HOME/data/health-port"
14
+ HEALTH_PORT=$(cat "$HEALTH_PORT_FILE" 2>/dev/null || echo "19579")
15
+ HEALTH_URL="http://127.0.0.1:${HEALTH_PORT}/healthz"
16
+ NPM_PACKAGE="grix-connector"
17
+
18
+ # Ensure directories exist
19
+ mkdir -p "$GRIX_HOME/log" "$GRIX_HOME/data"
20
+
21
+ log() {
22
+ echo "[$(date '+%Y-%m-%dT%H:%M:%S%z')] guardian: $1" >> "$LOG_FILE"
23
+ }
24
+
25
+ # Prevent duplicate guardian instances
26
+ if [ -f "$LOCK_FILE" ]; then
27
+ LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
28
+ if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
29
+ exit 0
30
+ fi
31
+ rm -f "$LOCK_FILE"
32
+ fi
33
+ echo $$ > "$LOCK_FILE"
34
+ trap 'rm -f "$LOCK_FILE"' EXIT
35
+
36
+ # Read pending marker
37
+ if [ ! -f "$PENDING_FILE" ]; then
38
+ exit 0
39
+ fi
40
+
41
+ # Extract fields from JSON (portable: no jq dependency)
42
+ FROM_VERSION=$(cat "$PENDING_FILE" | grep -o '"from_version":"[^"]*"' | cut -d'"' -f4)
43
+ TARGET_VERSION=$(cat "$PENDING_FILE" | grep -o '"target_version":"[^"]*"' | cut -d'"' -f4)
44
+ UPGRADED_AT=$(cat "$PENDING_FILE" | grep -o '"upgraded_at":"[^"]*"' | cut -d'"' -f4)
45
+ CRASH_COUNT=$(cat "$PENDING_FILE" | grep -o '"crash_count":[0-9]*' | cut -d: -f2)
46
+ CRASH_COUNT=${CRASH_COUNT:-0}
47
+
48
+ # Validate pending file
49
+ if [ -z "$FROM_VERSION" ]; then
50
+ log "pending file corrupt, missing from_version, removing"
51
+ rm -f "$PENDING_FILE"
52
+ exit 0
53
+ fi
54
+
55
+ log "started: from=$FROM_VERSION target=$TARGET_VERSION crash_count=$CRASH_COUNT"
56
+
57
+ # Wait for old daemon to exit (SIGTERM + shutdown + restart)
58
+ sleep 15
59
+
60
+ # Monitor loop: wait for healthz to return 200
61
+ ELAPSED=0
62
+ while [ $ELAPSED -lt $HEALTH_TIMEOUT ]; do
63
+ if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
64
+ # healthz 通过=新版健康。这里不删 pending:删了会抢在新进程的
65
+ # handlePendingOnStartup 之前,使其读不到 pending、success 回执发不出去
66
+ # (后端只见 installed 不见 success)。交由新进程上报 success 后自行删 pending,
67
+ # guardian 只负责崩溃回滚。
68
+ log "healthz passed, upgrade healthy; leaving pending for in-process success report"
69
+ exit 0
70
+ fi
71
+ sleep 3
72
+ ELAPSED=$((ELAPSED + 3))
73
+ done
74
+
75
+ # Extra grace period for slow systems
76
+ sleep 15
77
+ if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
78
+ # 同上:不删 pending,交由进程内 handlePendingOnStartup 上报 success 并删除。
79
+ log "healthz passed (delayed), upgrade healthy; leaving pending for in-process success report"
80
+ exit 0
81
+ fi
82
+
83
+ # Health check failed
84
+ CRASH_COUNT=$((CRASH_COUNT + 1))
85
+ log "healthz timeout, crash_count=$CRASH_COUNT"
86
+
87
+ if [ $CRASH_COUNT -ge $MAX_CRASHES ]; then
88
+ log "rollback: installing ${NPM_PACKAGE}@${FROM_VERSION}"
89
+ # P5: 先用默认源,失败再显式回退官方 npm(默认源可能是淘宝镜像,旧版若没同步同样会失败)。
90
+ if npm install -g "${NPM_PACKAGE}@${FROM_VERSION}" --prefer-online --no-audit --no-fund >> "$LOG_FILE" 2>&1 \
91
+ || { log "rollback via default registry failed, retrying official npm"; \
92
+ npm install -g "${NPM_PACKAGE}@${FROM_VERSION}" --registry https://registry.npmjs.org --prefer-online --no-audit --no-fund >> "$LOG_FILE" 2>&1; }; then
93
+ log "rollback succeeded"
94
+ rm -f "$PENDING_FILE"
95
+ else
96
+ log "ROLLBACK FAILED — manual intervention required"
97
+ fi
98
+ exit 0
99
+ fi
100
+
101
+ # Under threshold — update crash_count and exit, let process manager restart
102
+ echo "{\"from_version\":\"$FROM_VERSION\",\"target_version\":\"$TARGET_VERSION\",\"upgraded_at\":\"$UPGRADED_AT\",\"crash_count\":$CRASH_COUNT}" > "$PENDING_FILE.tmp"
103
+ mv "$PENDING_FILE.tmp" "$PENDING_FILE"
104
+ log "updated crash_count to $CRASH_COUNT, exiting"
@@ -1 +0,0 @@
1
- import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const r of t)e.push(r);const n=Buffer.concat(e).toString("utf8").trim();return n?JSON.parse(n):{}}function k(t){const e=(t.headers.authorization??"").trim();return e.toLowerCase().startsWith("bearer ")?e.slice(7).trim():""}class w{host="127.0.0.1";port=0;token;callbacks;server=null;address=null;constructor(e){this.token=d(),this.callbacks=e}getToken(){return this.token}getURL(){return this.address?`http://${this.address.address}:${this.address.port}`:""}async start(){this.server||(this.server=c.createServer(async(e,n)=>{try{await this.handleRequest(e,n)}catch(r){h(n,r instanceof Error?r.message:String(r))}}),await new Promise((e,n)=>{this.server.once("error",n),this.server.listen(this.port,this.host,()=>{this.server.off("error",n),e()})}),this.address=this.server.address(),o.info("claude-bridge",`Bridge server listening on ${this.getURL()}`))}async stop(){if(!this.server)return;const e=this.server;this.server=null,this.address=null,e.closeIdleConnections?.(),e.closeAllConnections?.(),await new Promise((n,r)=>{e.close(s=>s?r(s):n())})}async handleRequest(e,n){if(k(e)!==this.token){l(n);return}if(e.method!=="POST"){n.writeHead(405,{"content-type":"application/json"}),n.end(JSON.stringify({error:"method_not_allowed"}));return}const r=new URL(e.url,"http://localhost").pathname,s=await v(e),i=f.get(r);if(!i){u(n);return}const a=await i(this.callbacks,s);p(n,a??{ok:!0})}}const f=new Map([["/v1/worker/register",async(t,e)=>(o.info("claude-bridge",`Worker registered: ${e.worker_id} (pid=${e.pid})`),t.onRegisterWorker(e))],["/v1/worker/status",async(t,e)=>(o.info("claude-bridge",`Worker status: ${e.status}`),t.onStatusUpdate(e))],["/v1/worker/send-text",async(t,e)=>t.onSendText(e)],["/v1/worker/send-stream-chunk",async(t,e)=>t.onSendStreamChunk(e)],["/v1/worker/send-media",async(t,e)=>t.onSendMedia(e)],["/v1/worker/delete-message",async(t,e)=>t.onDeleteMessage(e)],["/v1/worker/ack-event",async(t,e)=>t.onAckEvent(e)],["/v1/worker/event-result",async(t,e)=>t.onSendEventResult(e)],["/v1/worker/event-stop-ack",async(t,e)=>t.onSendEventStopAck(e)],["/v1/worker/event-stop-result",async(t,e)=>t.onSendEventStopResult(e)],["/v1/worker/session-composing",async(t,e)=>t.onSetSessionComposing(e)],["/v1/worker/agent-invoke",async(t,e)=>t.onAgentInvoke(e)],["/v1/worker/local-action-result",async(t,e)=>t.onLocalActionResult(e)]]);export{w as ClaudeBridgeServer};
@@ -1 +0,0 @@
1
- import{randomUUID as x}from"node:crypto";import{CallToolRequestSchema as S,ListToolsRequestSchema as w}from"@modelcontextprotocol/sdk/types.js";import{log as f}from"../../core/log/index.js";import{toolCallToInvoke as k}from"../../core/mcp/tools.js";const E=new Set(["contact_search","session_search","message_history","message_search","group_create","group_detail_read","group_leave_self","group_member_add","group_member_remove","group_member_role_update","group_all_members_muted_update","group_member_speaking_update","group_dissolve","send_msg","delete_msg","agent_api_create","agent_category_list","agent_category_create","agent_category_update","agent_category_assign","agent_api_key_rotate"]),y=3e4,I=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},files:{type:"array",items:{type:"string"},description:"Optional absolute local file paths. Each file is uploaded through Agent API OSS presign before sending."},final:{type:"boolean",description:"Whether this is the final reply for the event. Defaults to false \u2014 the event stays open while Claude continues working, and auto-completes after inactivity. Set true only when this is definitively the last message for the event."}},required:["chat_id","event_id"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},code:{type:"string"},msg:{type:"string"}},required:["event_id","status"]}},{name:"delete_message",description:"Delete a previously sent message in the same grix-claude chat.",inputSchema:{type:"object",properties:{chat_id:{type:"string"},message_id:{type:"string"}},required:["chat_id","message_id"]}},{name:"status",description:"Show grix-claude runtime status, upstream access state, bridge health, and startup hints.",inputSchema:{type:"object",properties:{}}},{name:"send",description:"Send a message to a chat session proactively, without requiring an inbound event. Use for notifications or scheduled reports.",inputSchema:{type:"object",properties:{chat_id:{type:"string",description:"The target chat/session id."},text:{type:"string",description:"The message text to send."}},required:["chat_id","text"]}},{name:"access_pair",description:"Forward a sender pairing approval code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"access_deny",description:"Forward a sender pairing denial code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"allow_sender",description:"Ask upstream access control to allow a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"remove_sender",description:"Ask upstream access control to remove a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"access_policy",description:"Ask upstream access control to update the sender access policy.",inputSchema:{type:"object",properties:{policy:{type:"string",enum:["allowlist","open","disabled"]}},required:["policy"]}},{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},keyword:{type:"string"},id:{type:"string"},sessionId:{type:"string"},limit:{type:"number"},offset:{type:"number"},beforeId:{type:"string"}},required:["action"]}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string"},memberIds:{type:"array",items:{type:"string"}},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer",description:"Member type (for update_member_role / update_member_speaking)."},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted (for update_member_speaking)."}},required:["action"]}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},content:{type:"string"},msgType:{type:"number"},quotedMessageId:{type:"string"},threadId:{type:"string"}},required:["sessionId","content"]}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},msgId:{type:"string"}},required:["sessionId","msgId"]}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentId:{type:"string"},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"number"}},required:["action"]}}];function q(r,e){r.setRequestHandler(w,async()=>({tools:I})),r.setRequestHandler(S,async i=>{const{name:n,arguments:t}=i.params,s=t??{};try{switch(n){case"reply":return await A(s,e);case"complete":return await $(s,e);case"delete_message":return await T(s,e);case"status":return j(e);case"send":return await M(s,e);case"access_pair":case"access_deny":case"allow_sender":case"remove_sender":case"access_policy":return await R(n,s,e);case"grix_query":case"grix_group":case"grix_message_send":case"grix_message_unsend":case"grix_admin":return await O(n,s,e);default:return{content:[{type:"text",text:`Unknown tool: ${n}`}],isError:!0}}}catch(a){return f.error("claude-tools",`Tool ${n} error: ${a}`),{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}})}async function A(r,e){const i=e.getActiveEvent();if(!i)return{content:[{type:"text",text:"No active event to reply to"}],isError:!0};const n=String(r.text??""),t=String(r.chat_id??""),s=String(r.event_id??i.eventId),a=r.reply_to,d=r.files,_=r.final===!0;if(!t||!s)return{content:[{type:"text",text:"reply requires chat_id and event_id"}],isError:!0};if(!n.trim()&&(!d||d.length===0))return{content:[{type:"text",text:"reply requires at least one of text or files"}],isError:!0};const{text:g,quotedMessageId:v}=e.resolveQuotedMessageId(a,n),b=[];let m=0;const h=`reply_${s}_${Date.now()}`;try{if(g){const p=e.splitText(g);for(let o=0;o<p.length;o++){if(!e.isEventActive(s))return c("ignored: event no longer active");m++,e.bridge.sendStreamChunk(s,t,p[o],++i.chunkSeq,!1,h)}}if(d&&d.length>0)for(const p of d){if(!e.isEventActive(s))return c("ignored: event no longer active");m++;const o=await e.uploadFile(p,t),l=`${x()}_${m}`;e.bridge.sendMedia(s,t,o.access_url,o.file_name,v,l,o.extra),f.info("claude-tools",`File sent: ${o.file_name}`)}e.bridge.sendStreamChunk(s,t,"",++i.chunkSeq,!0,h)}catch(p){if(g&&b.length===0)try{const o=`fallback_${s}_${Date.now()}`,l=e.splitText(g);for(let u=0;u<l.length;u++)e.bridge.sendStreamChunk("",t,l[u],u+1,!1,o);return e.bridge.sendStreamChunk("",t,"",l.length+1,!0,o),e.markReplySent(s),_&&e.finalizeEvent(s,"responded"),c("sent via fallback")}catch{}if(!e.isEventActive(s))return c("ignored: event no longer active");throw e.bridge.sendEventResult(s,"failed",String(p),"send_msg_failed"),p}return e.markReplySent(s),_?e.finalizeEvent(s,"responded"):(i.responded=!0,e.clearActiveEvent("completed")),c("Reply sent")}async function $(r,e){const i=e.getActiveEvent(),n=String(r.event_id??""),t=r.status??"",s=r.code,a=r.msg;if(!n||!t)return{content:[{type:"text",text:"complete requires event_id and status"}],isError:!0};const d=["responded","canceled","failed"];return d.includes(t)?e.isEventActive(n)?(e.bridge.sendEventResult(n,t,a,s),e.clearActiveEvent(t),c("Event completed")):c("ignored: event no longer active"):{content:[{type:"text",text:`status must be one of: ${d.join(", ")}`}],isError:!0}}async function T(r,e){const i=String(r.chat_id??""),n=String(r.message_id??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and message_id are required"}],isError:!0};try{return await e.bridge.agentInvoke("grix_message_unsend",{sessionId:i,msgId:n},y),c(`deleted (${n})`)}catch(t){return{content:[{type:"text",text:`Delete failed: ${t}`}],isError:!0}}}function j(r){const e=r.getStatusInfo();return{content:[{type:"text",text:JSON.stringify({alive:e.alive,active_event:e.activeEvent,pending_approvals:e.pendingPermissions,pending_questions:e.pendingElicitations})}]}}async function M(r,e){const i=String(r.chat_id??""),n=String(r.text??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and text are required"}],isError:!0};try{const t=e.splitText(n),s=`send_${i}_${Date.now()}`;for(let a=0;a<t.length;a++)e.bridge.sendStreamChunk("",i,t[a],a+1,!1,s);return e.bridge.sendStreamChunk("",i,"",t.length+1,!0,s),c("sent")}catch(t){return{content:[{type:"text",text:`Send failed: ${t}`}],isError:!0}}}const C={access_pair:{verb:"pair_approve",payloadKey:"code"},access_deny:{verb:"pair_deny",payloadKey:"code"},allow_sender:{verb:"sender_allow",payloadKey:"sender_id"},remove_sender:{verb:"sender_remove",payloadKey:"sender_id"},access_policy:{verb:"policy_set",payloadKey:"policy"}};async function R(r,e,i){try{const n=C[r];if(!n)throw new Error(`Unknown access control tool: ${r}`);const t={};e.code!=null&&(t.code=e.code),e.sender_id!=null&&(t.sender_id=e.sender_id),e.policy!=null&&(t.policy=e.policy);const s=await i.bridge.agentInvoke("claude_access_control",{verb:n.verb,payload:t},y);return{content:[{type:"text",text:typeof s=="string"?s:JSON.stringify(s)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}async function O(r,e,i){try{const n=k(r,e);if(!E.has(n.action))throw new Error(`Action not allowed: ${n.action}`);const t=await i.bridge.agentInvoke(n.action,n.params,y);if(t&&Number(t.code??0)!==0)throw new Error(String(t.msg??"invoke failed"));return{content:[{type:"text",text:t?.data!=null?typeof t.data=="string"?t.data:JSON.stringify(t.data):JSON.stringify(t)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}function c(r){return{content:[{type:"text",text:r}]}}export{q as registerClaudeTools};
@@ -1 +0,0 @@
1
- import{log as l}from"../../core/log/index.js";class c{controlURL="";token="";isConfigured(){return!!(this.controlURL&&this.token)}configure(r,e){this.controlURL=r.replace(/\/+$/,""),this.token=e.trim(),l.info("claude-worker-client",`Configured with control URL: ${this.controlURL}`)}async post(r,e,s){if(!this.isConfigured())throw new Error("worker control not configured");const i=new AbortController,o=setTimeout(()=>i.abort(),s);try{const t=await fetch(`${this.controlURL}${r}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.token}`},body:JSON.stringify(e),signal:i.signal}),n=await t.text(),a=n.trim()?JSON.parse(n):{};if(!t.ok)throw new Error(a.error||`worker control failed ${t.status}`);return a}finally{clearTimeout(o)}}isRetryableError(r){const e=r instanceof Error?r.message:String(r);return/fetch failed|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|aborted/i.test(e)}async postWithRetry(r,e,s,i=1){let o;for(let t=0;t<=i;t++)try{return t>0&&l.info("claude-worker-client",`Retrying ${r} attempt=${t+1}`),await this.post(r,e,s)}catch(n){if(o=n,t>=i||!this.isRetryableError(n))break;await new Promise(a=>setTimeout(a,150))}throw o instanceof Error?o:new Error(String(o))}async deliverEvent(r){return this.postWithRetry("/v1/worker/deliver-event",{payload:r},1e4,1)}async deliverStop(r){return this.postWithRetry("/v1/worker/deliver-stop",{payload:r},1e4,1)}async deliverLocalAction(r){return this.postWithRetry("/v1/worker/deliver-local-action",{payload:r},1e4,1)}async ping(){try{return await this.postWithRetry("/v1/worker/ping",{},5e3,1),!0}catch{return!1}}}export{c as ClaudeWorkerClient};
@@ -1,2 +0,0 @@
1
- import{spawn as x,execSync as y}from"node:child_process";import{randomUUID as v}from"node:crypto";import{mkdir as S}from"node:fs/promises";import{readFileSync as C}from"node:fs";import{join as d}from"node:path";import{homedir as T,tmpdir as I}from"node:os";import{log as o}from"../../core/log/index.js";import{MCP_HTTP_CHANNEL_NAME as m}from"./protocol-contract.js";function P(t){let e=null,r=0,n=!1,i=!1;const a=v(),s=t.gatewayUrl??"http://127.0.0.1:19580/mcp";return{async start(){await F(t.command,s,t.env);const c=E(t.grix),u=[...t.args??[],"--name",`grix-mcp-${t.name}`,"--session-id",a];t.fullAuto&&u.push("--dangerously-skip-permissions"),u.push("--dangerously-load-development-channels",`server:${m}`,"--append-system-prompt",c);const f=d(I(),`grix-mcp-claude-${t.name}`);await S(f,{recursive:!0});const{expectPath:$,pidPath:g}=await M(f,t.command,u),_={...process.env,...t.env??{}};e=x("/usr/bin/expect",[$],{cwd:t.cwd,env:_,stdio:["ignore","pipe","pipe"],detached:!0}),o.info("mcp-http-launcher",`\u542F\u52A8 Claude: name=${t.name} cwd=${t.cwd} pid=${e.pid}`),r=await k(g),n=!0,o.info("mcp-http-launcher",`Claude \u5B50\u8FDB\u7A0B PID: ${r}`),e.on("exit",(l,p)=>{o.info("mcp-http-launcher",`Claude \u9000\u51FA: code=${l} signal=${p}`),n=!1,e=null,r=0,i||(o.info("mcp-http-launcher","3 \u79D2\u540E\u81EA\u52A8\u91CD\u542F..."),setTimeout(()=>{i||this.start().catch(w=>{o.error("mcp-http-launcher",`\u91CD\u542F\u5931\u8D25: ${w}`)})},3e3))}),e.stdout?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stdout] ${p.slice(0,300)}`)}),e.stderr?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stderr] ${p.slice(0,300)}`)})},async stop(){if(i=!0,n=!1,r>0)try{process.kill(r,"SIGTERM")}catch{}if(e?.pid){try{process.kill(-e.pid,"SIGTERM")}catch{}await new Promise(c=>{const u=setTimeout(()=>{if(r>0)try{process.kill(r,"SIGKILL")}catch{}if(e?.pid)try{process.kill(-e.pid,"SIGKILL")}catch{}c()},5e3);e?.once("exit",()=>{clearTimeout(u),c()})})}e=null,r=0},getStatus(){return{name:t.name,alive:n,pid:r}}}}function E(t){return["You are connected to a chat via the grix MCP server.",`On startup, immediately call grix_authorize with: agentId="${t.agentId}", apiKey="${t.apiKey}", wsUrl="${t.wsUrl}", clientType="${t.clientType}".`,"When you receive a <channel> message, you MUST respond by calling the grix_reply tool (or the grix_complete tool if no response is needed).","Never write your reply as plain text \u2014 it will NOT reach the user. Only the grix_reply tool delivers your response to the chat.","The <channel> message contains event_id and session_id \u2014 pass them to grix_reply."].join(" ")}async function F(t,e,r){const n=d(T(),".claude.json");let i=null;try{const s=C(n,"utf8");i=JSON.parse(s)?.mcpServers?.[m]??null}catch{}if(i&&String(i.type??"").trim()==="http"&&String(i.url??"").trim()===e)return;o.info("mcp-http-launcher",`\u6CE8\u518C MCP Server: ${m} -> ${e}`);const a={...process.env,...r??{}};try{y(`${t} mcp remove -s user ${m}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}catch{}y(`${t} mcp add --scope user --transport http ${m} ${e}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}async function M(t,e,r){const{writeFile:n}=await import("node:fs/promises"),i=d(t,"claude.pid"),a=d(t,"claude.expect"),s=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set claude_command [list {${h(e)}}${r.map(c=>` {${h(c)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${h(i)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)I am using this for local development} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {Listening for channel} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," -re {bypass permissions} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," eof {}","}","expect eof",""];return await n(i,"","utf8"),await n(a,s.join(`
2
- `),"utf8"),{expectPath:a,pidPath:i}}function h(t){return t.replace(/[\\{}$\[\]"]/g,"\\$&")}async function k(t,e=1e4){const{readFile:r}=await import("node:fs/promises"),n=Math.ceil(e/100);for(let i=0;i<n;i++){try{const a=await r(t,"utf8"),s=parseInt(String(a).trim(),10);if(Number.isFinite(s)&&s>0)return s}catch{}await new Promise(a=>setTimeout(a,100))}return 0}export{P as createMcpHttpLauncher};
@@ -1 +0,0 @@
1
- class m{defaultTimeoutMs;onTimeout;timers=new Map;constructor(e){this.defaultTimeoutMs=e.defaultTimeoutMs??9e4,this.onTimeout=e.onTimeout}arm(e,t){this.cancel(e);const s=t?.timeoutMs??this.defaultTimeoutMs,i=Date.now()+s,o=setTimeout(()=>{this.timers.delete(e),this.onTimeout(e).catch(()=>{})},s);return this.timers.set(e,o),i}cancel(e){const t=this.timers.get(e);t&&(clearTimeout(t),this.timers.delete(e))}has(e){return this.timers.has(e)}close(){for(const e of this.timers.values())clearTimeout(e);this.timers.clear()}}export{m as ResultTimeoutManager};