coze_lab 0.1.13 → 0.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.
package/README.md CHANGED
@@ -8,6 +8,9 @@ Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to Coz
8
8
  # First-time setup — triggers browser OAuth authorization
9
9
  npx coze_lab --agent=<type>
10
10
 
11
+ # Cloud setup for a managed agent
12
+ npx coze_lab --cloud --agent-id=<agentId>
13
+
11
14
  # Auth-only commands (no agent configuration)
12
15
  npx coze_lab --login # Device Code login only
13
16
  npx coze_lab --status # Show current authorization status
@@ -20,6 +23,9 @@ npx coze_lab --logout # Clear cached credentials
20
23
  | Parameter | Required | Values / Effect |
21
24
  |-----------|----------|-----------------|
22
25
  | `--agent` | ✓ (for setup) | `claude-code`, `codex`, `openclaw` |
26
+ | `--agent-id` | — | Resolve `~/.coze/agents/<agentId>/config.json` and write per-agent config |
27
+ | `--cloud` | — | Cloud mode: read token from env and emit `COZE_LAB_RESULT=...` |
28
+ | `--codex-home` | — | Override Codex config home for non-cloud/custom runs |
23
29
  | `--login` | — | Run the Device Code login flow only |
24
30
  | `--status` | — | Print local token status (valid / expiring / expired) |
25
31
  | `--refresh` | — | Force-refresh the access token via `refresh_token` |
@@ -50,6 +56,11 @@ npx coze_lab --logout # Clear cached credentials
50
56
  | `codex` | `~/.codex/hooks/cozeloop_hook.py` | `~/.codex/hooks.json` | `~/.codex/hooks/cozeloop.env` |
51
57
  | `openclaw` | — (Node.js plugin) | `~/.openclaw/openclaw.json` | inline in config |
52
58
 
59
+ For cloud Codex with `--cloud --agent-id=<agentId>`, Codex hooks are written to
60
+ `~/.coze/agents/<agentId>/codex-home` by default. The directory is created if it
61
+ does not already exist, so callers do not need to pass `--codex-home` for the
62
+ standard coze-bridge layout.
63
+
53
64
  ## Token lifecycle
54
65
 
55
66
  OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
package/index.js CHANGED
@@ -4618,23 +4618,118 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4618
4618
  return { hookScript, envFile, hooksJson };
4619
4619
  }
4620
4620
 
4621
+ function resolveCodexHome(args) {
4622
+ if (args.cloud && args.agentId) {
4623
+ return path.join(os.homedir(), '.coze', 'agents', args.agentId, 'codex-home');
4624
+ }
4625
+ return args['codex-home'] || process.env.CODEX_HOME || undefined;
4626
+ }
4627
+
4621
4628
  // writeOpenClawHook 配置 OpenClaw 的 cozeloop-trace 插件(全局装在 ~/.openclaw)。
4622
4629
  // agentId 非空时并入 plugins.entries[...].config.traceAgentIds allowlist —— 插件运行时
4623
4630
  // 用 resolveAgentIdFromHookCtx 取当前 agentId,仅 allowlist 内的 agent 才上报 trace。
4624
4631
  // allowlist 为空(本地全局模式)= 全部放行,向后兼容。
4632
+ // resolveHomeDir 解析 home 目录。云端 SandboxShellExec 执行环境的 $HOME 可能缺失/不一致,
4633
+ // 导致 os.homedir() 解析不到真实 home(如 /root)。云端模式下做兜底探测。
4634
+ function resolveHomeDir(cloud) {
4635
+ const h = os.homedir();
4636
+ if (!cloud) return h;
4637
+ // 优先 $HOME,其次 os.homedir(),再回退云端常见 root home。
4638
+ const candidates = [process.env.HOME, h, '/root'].filter(Boolean);
4639
+ for (const c of candidates) {
4640
+ try {
4641
+ if (fs.existsSync(path.join(c, '.coze')) || fs.existsSync(path.join(c, '.openclaw'))) {
4642
+ return c;
4643
+ }
4644
+ } catch { /* ignore */ }
4645
+ }
4646
+ return h || '/root';
4647
+ }
4648
+
4649
+ function normalizeTraceAgentIds(ids) {
4650
+ return (Array.isArray(ids) ? ids : [])
4651
+ .map((s) => String(s).trim().toLowerCase())
4652
+ .filter(Boolean);
4653
+ }
4654
+
4655
+ function getOpenClawEndpoint(cloud) {
4656
+ return (cloud && process.env.COZELOOP_API_BASE_URL)
4657
+ ? process.env.COZELOOP_API_BASE_URL.replace(/\/+$/, '') + '/v1/loop/opentelemetry'
4658
+ : 'https://api.coze.cn/v1/loop/opentelemetry';
4659
+ }
4660
+
4661
+ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud) {
4662
+ if (!existing.plugins) existing.plugins = {};
4663
+ if (!existing.plugins.allow) existing.plugins.allow = [];
4664
+ if (!existing.plugins.entries) existing.plugins.entries = {};
4665
+
4666
+ const PLUGIN = 'openclaw-cozeloop-trace';
4667
+ if (!existing.plugins.allow.includes(PLUGIN)) {
4668
+ existing.plugins.allow.push(PLUGIN);
4669
+ }
4670
+ // Preserve existing entry structure, only update config.
4671
+ if (!existing.plugins.entries[PLUGIN]) {
4672
+ existing.plugins.entries[PLUGIN] = { enabled: true };
4673
+ }
4674
+ existing.plugins.entries[PLUGIN].enabled = true;
4675
+ // hooks.allowConversationAccess required for 2026.5+ to access session content.
4676
+ existing.plugins.entries[PLUGIN].hooks = { allowConversationAccess: true };
4677
+ if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
4678
+ const pcfg = existing.plugins.entries[PLUGIN].config;
4679
+ pcfg.authorization = `Bearer ${token}`;
4680
+ pcfg.endpoint = getOpenClawEndpoint(cloud);
4681
+ pcfg.workspaceId = workspaceId;
4682
+ pcfg.debug = true;
4683
+ // per-agent trace 放行:把当前 agentId 并入 traceAgentIds(去重、归一为小写,
4684
+ // 与插件侧 resolveAgentIdFromHookCtx 的归一一致)。无 agentId(全局模式)则不动
4685
+ // allowlist —— 空 allowlist 表示全部放行。
4686
+ if (agentId) {
4687
+ const norm = String(agentId).trim().toLowerCase();
4688
+ const list = normalizeTraceAgentIds(pcfg.traceAgentIds);
4689
+ if (norm && !list.includes(norm)) list.push(norm);
4690
+ pcfg.traceAgentIds = list;
4691
+ }
4692
+ return existing;
4693
+ }
4694
+
4695
+ function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud) {
4696
+ if (!fs.existsSync(pluginDir)) return false;
4697
+ let existing;
4698
+ try {
4699
+ existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
4700
+ } catch {
4701
+ return false;
4702
+ }
4703
+ const desired = applyOpenClawPluginConfig(
4704
+ JSON.parse(JSON.stringify(existing)),
4705
+ token,
4706
+ workspaceId,
4707
+ agentId,
4708
+ cloud,
4709
+ );
4710
+ return JSON.stringify(existing) === JSON.stringify(desired);
4711
+ }
4712
+
4625
4713
  function writeOpenClawHook(token, workspaceId, agentId, cloud) {
4626
- const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
4627
- const pluginDir = path.join(os.homedir(), '.cozeloop', 'openclaw-plugin');
4714
+ const home = resolveHomeDir(cloud);
4715
+ const configPath = path.join(home, '.openclaw', 'openclaw.json');
4716
+ const pluginDir = path.join(home, '.cozeloop', 'openclaw-plugin');
4628
4717
 
4629
4718
  if (!fs.existsSync(configPath)) {
4630
4719
  errorBox([
4631
- 'ERROR: ~/.openclaw/openclaw.json not found',
4720
+ `ERROR: openclaw.json not found at ${configPath}`,
4632
4721
  '',
4633
4722
  'Make sure OpenClaw is installed and has been run at least once.',
4634
- 'Install: npm install -g openclaw',
4723
+ `(home=${home}, $HOME=${process.env.HOME || 'unset'})`,
4635
4724
  ]);
4636
4725
  }
4637
4726
 
4727
+ if (isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud)) {
4728
+ ok(`OpenClaw plugin already configured in ${configPath}`);
4729
+ info('OpenClaw gateway restart skipped (configuration unchanged).');
4730
+ return { configPath, pluginDir, unchanged: true };
4731
+ }
4732
+
4638
4733
  // 1. Write plugin files to ~/.cozeloop/openclaw-plugin/
4639
4734
  ensureDir(pluginDir);
4640
4735
  ensureDir(path.join(pluginDir, 'dist'));
@@ -4685,46 +4780,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud) {
4685
4780
 
4686
4781
  // 4. Update openclaw.json with token and workspace
4687
4782
  const config = mergeJson(configPath, (existing) => {
4688
- if (!existing.plugins) existing.plugins = {};
4689
- if (!existing.plugins.allow) existing.plugins.allow = [];
4690
- if (!existing.plugins.entries) existing.plugins.entries = {};
4691
-
4692
- const PLUGIN = 'openclaw-cozeloop-trace';
4693
- if (!existing.plugins.allow.includes(PLUGIN)) {
4694
- existing.plugins.allow.push(PLUGIN);
4695
- }
4696
- // Preserve existing entry structure, only update config
4697
- if (!existing.plugins.entries[PLUGIN]) {
4698
- existing.plugins.entries[PLUGIN] = { enabled: true };
4699
- }
4700
- existing.plugins.entries[PLUGIN].enabled = true;
4701
- // hooks.allowConversationAccess required for 2026.5+ to access session content
4702
- existing.plugins.entries[PLUGIN].hooks = { allowConversationAccess: true };
4703
- if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
4704
- const pcfg = existing.plugins.entries[PLUGIN].config;
4705
- pcfg.authorization = `Bearer ${token}`;
4706
- // 云端:endpoint 走 sandbox 注入的 COZELOOP_API_BASE_URL 代理(token 经它鉴权);
4707
- // 缺省回退 api.coze.cn 直连。插件 exporter 会在此基础上拼 /v1/traces。
4708
- const ocBase = (cloud && process.env.COZELOOP_API_BASE_URL)
4709
- ? process.env.COZELOOP_API_BASE_URL.replace(/\/+$/, '') + '/v1/loop/opentelemetry'
4710
- : 'https://api.coze.cn/v1/loop/opentelemetry';
4711
- pcfg.endpoint = ocBase;
4712
- pcfg.workspaceId = workspaceId;
4713
- pcfg.debug = true;
4714
- // per-agent trace 放行:把当前 agentId 并入 traceAgentIds(去重、归一为小写,
4715
- // 与插件侧 resolveAgentIdFromHookCtx 的归一一致)。无 agentId(全局模式)则不动
4716
- // allowlist —— 空 allowlist 表示全部放行。
4717
- if (agentId) {
4718
- const norm = String(agentId).trim().toLowerCase();
4719
- // 读 existing 时一并归一(小写、去空),与插件侧 resolveAgentIdFromHookCtx 一致,
4720
- // 防手工编辑混入大写条目导致去重失效。
4721
- const list = (Array.isArray(pcfg.traceAgentIds) ? pcfg.traceAgentIds : [])
4722
- .map((s) => String(s).trim().toLowerCase())
4723
- .filter(Boolean);
4724
- if (norm && !list.includes(norm)) list.push(norm);
4725
- pcfg.traceAgentIds = list;
4726
- }
4727
- return existing;
4783
+ return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud);
4728
4784
  });
4729
4785
 
4730
4786
  try {
@@ -5241,8 +5297,11 @@ async function main() {
5241
5297
  }
5242
5298
  written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
5243
5299
  } else if (agent === 'codex') {
5244
- // CODEX_HOME 来源优先级:--codex-home= > 环境变量 CODEX_HOME > ~/.codex(缺省)。
5245
- const codexHome = args['codex-home'] || process.env.CODEX_HOME || undefined;
5300
+ const codexHome = resolveCodexHome(args);
5301
+ if (args.cloud && args.agentId && codexHome && !fs.existsSync(codexHome)) {
5302
+ ensureDir(codexHome);
5303
+ info(`已创建云端 Codex 配置目录: ${codexHome}`);
5304
+ }
5246
5305
  if (codexHome) info(`Codex 配置目录: ${codexHome}`);
5247
5306
  written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
5248
5307
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -16,7 +16,12 @@
16
16
  },
17
17
  "files": [
18
18
  "index.js",
19
- "scripts/"
19
+ "scripts/claude-code/cozeloop_hook.py",
20
+ "scripts/codex/cozeloop_hook.py",
21
+ "scripts/shared/cozeloop_refresh.py",
22
+ "scripts/openclaw/dist/",
23
+ "scripts/openclaw/openclaw.plugin.json",
24
+ "scripts/openclaw/package.json"
20
25
  ],
21
26
  "engines": {
22
27
  "node": ">=18"
@@ -29,15 +29,53 @@ from pathlib import Path
29
29
  from typing import Optional, List, Dict, Any
30
30
 
31
31
  # --- SDK Import ---
32
- try:
32
+ def _ensure_cozeloop_sdk():
33
+ try:
34
+ import cozeloop # noqa: F401
35
+ return True
36
+ except ImportError:
37
+ pass
38
+ import subprocess
39
+ import importlib
40
+ import site
41
+ attempts = (
42
+ ["--quiet", "--disable-pip-version-check", "cozeloop"],
43
+ ["--quiet", "--disable-pip-version-check", "--break-system-packages", "cozeloop"],
44
+ ["--quiet", "--disable-pip-version-check", "--break-system-packages", "--user", "cozeloop"],
45
+ )
46
+ for extra in attempts:
47
+ try:
48
+ subprocess.run(
49
+ [sys.executable, "-m", "pip", "install", *extra],
50
+ timeout=180, check=True,
51
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
52
+ )
53
+ except Exception:
54
+ continue
55
+ try:
56
+ importlib.reload(site)
57
+ user_site = site.getusersitepackages()
58
+ for p in ([user_site] if isinstance(user_site, str) else list(user_site)):
59
+ if p and p not in sys.path:
60
+ sys.path.insert(0, p)
61
+ importlib.invalidate_caches()
62
+ import cozeloop # noqa: F401
63
+ print("[CozeLoop] cozeloop SDK auto-installed at runtime.", file=sys.stderr)
64
+ return True
65
+ except ImportError:
66
+ continue
67
+ return False
68
+
69
+
70
+ if _ensure_cozeloop_sdk():
33
71
  import cozeloop
34
72
  from cozeloop.spec.tracespec import (
35
73
  Runtime, ModelInput, ModelMessage, ModelToolChoice,
36
74
  ModelOutput, ModelChoice, ModelToolCall, ModelToolCallFunction,
37
75
  ModelMessagePart, ModelMessagePartType
38
76
  )
39
- except ImportError:
40
- print("Error: cozeloop SDK not found. Please install it with: pip install cozeloop", file=sys.stderr)
77
+ else:
78
+ print("Error: cozeloop SDK not found and auto-install failed. Try: pip install cozeloop", file=sys.stderr)
41
79
  sys.exit(1)
42
80
 
43
81
  # --- Configuration ---
@@ -1412,4 +1450,3 @@ if __name__ == "__main__":
1412
1450
  main()
1413
1451
 
1414
1452
 
1415
-
@@ -202,15 +202,53 @@ def get_fresh_token():
202
202
  # -------------------------------------------------------------------------
203
203
 
204
204
  # --- SDK Import ---
205
- try:
205
+ def _ensure_cozeloop_sdk():
206
+ try:
207
+ import cozeloop # noqa: F401
208
+ return True
209
+ except ImportError:
210
+ pass
211
+ import subprocess
212
+ import importlib
213
+ import site
214
+ attempts = (
215
+ ["--quiet", "--disable-pip-version-check", "cozeloop"],
216
+ ["--quiet", "--disable-pip-version-check", "--break-system-packages", "cozeloop"],
217
+ ["--quiet", "--disable-pip-version-check", "--break-system-packages", "--user", "cozeloop"],
218
+ )
219
+ for extra in attempts:
220
+ try:
221
+ subprocess.run(
222
+ [sys.executable, "-m", "pip", "install", *extra],
223
+ timeout=180, check=True,
224
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
225
+ )
226
+ except Exception:
227
+ continue
228
+ try:
229
+ importlib.reload(site)
230
+ user_site = site.getusersitepackages()
231
+ for p in ([user_site] if isinstance(user_site, str) else list(user_site)):
232
+ if p and p not in sys.path:
233
+ sys.path.insert(0, p)
234
+ importlib.invalidate_caches()
235
+ import cozeloop # noqa: F401
236
+ print("[CozeLoop] cozeloop SDK auto-installed at runtime.", file=sys.stderr)
237
+ return True
238
+ except ImportError:
239
+ continue
240
+ return False
241
+
242
+
243
+ if _ensure_cozeloop_sdk():
206
244
  import cozeloop
207
245
  from cozeloop.spec.tracespec import (
208
246
  Runtime, ModelInput, ModelMessage, ModelToolChoice,
209
247
  ModelOutput, ModelChoice, ModelToolCall, ModelToolCallFunction,
210
248
  ModelMessagePart, ModelMessagePartType
211
249
  )
212
- except ImportError:
213
- print("Error: cozeloop SDK not found. Please install it with: pip install cozeloop", file=sys.stderr)
250
+ else:
251
+ print("Error: cozeloop SDK not found and auto-install failed. Try: pip install cozeloop", file=sys.stderr)
214
252
  sys.exit(1)
215
253
 
216
254
  # --- Configuration ---
@@ -1120,4 +1158,3 @@ def main():
1120
1158
 
1121
1159
  if __name__ == "__main__":
1122
1160
  main()
1123
-