coze_lab 0.1.14 → 0.1.16

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,16 @@ 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
+
64
+ Codex hook diagnostics are appended to `hooks/cozeloop.log` under the same
65
+ Codex home. For cloud Codex, check
66
+ `~/.coze/agents/<agentId>/codex-home/hooks/cozeloop.log`. If that file is not
67
+ created after a new Codex turn, Codex did not load or execute the hook.
68
+
53
69
  ## Token lifecycle
54
70
 
55
71
  OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
package/index.js CHANGED
@@ -4554,6 +4554,7 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4554
4554
  const hooksDir = path.join(home, 'hooks');
4555
4555
  const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
4556
4556
  const envFile = path.join(hooksDir, 'cozeloop.env');
4557
+ const logFile = path.join(hooksDir, 'cozeloop.log');
4557
4558
  const hooksJson = path.join(home, 'hooks.json');
4558
4559
 
4559
4560
  // 1. Write Python hook scripts (trace + refresh)
@@ -4571,6 +4572,7 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4571
4572
  if (!cloud) {
4572
4573
  envLines.push(`COZELOOP_API_TOKEN=${token}`);
4573
4574
  }
4575
+ envLines.push(`COZELOOP_HOOK_LOG=${logFile}`);
4574
4576
  envLines.push('TRACE_TO_COZELOOP=true');
4575
4577
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4576
4578
  envLines.push(`x_tt_env=${PPE_TT_ENV}`);
@@ -4615,7 +4617,14 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4615
4617
  ok(`Hook registered in ${hooksJson}`);
4616
4618
  warn('Codex hook trust: 首次启动 Codex 会提示 "Hooks need review"。在该提示按 t(Trust all and continue),或在会话内运行 /hooks 后按 t,即可一次性信任全部 hook 启用 trace 上报。');
4617
4619
 
4618
- return { hookScript, envFile, hooksJson };
4620
+ return { hookScript, envFile, hooksJson, logFile };
4621
+ }
4622
+
4623
+ function resolveCodexHome(args) {
4624
+ if (args.cloud && args.agentId) {
4625
+ return path.join(os.homedir(), '.coze', 'agents', args.agentId, 'codex-home');
4626
+ }
4627
+ return args['codex-home'] || process.env.CODEX_HOME || undefined;
4619
4628
  }
4620
4629
 
4621
4630
  // writeOpenClawHook 配置 OpenClaw 的 cozeloop-trace 插件(全局装在 ~/.openclaw)。
@@ -5290,8 +5299,11 @@ async function main() {
5290
5299
  }
5291
5300
  written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
5292
5301
  } else if (agent === 'codex') {
5293
- // CODEX_HOME 来源优先级:--codex-home= > 环境变量 CODEX_HOME > ~/.codex(缺省)。
5294
- const codexHome = args['codex-home'] || process.env.CODEX_HOME || undefined;
5302
+ const codexHome = resolveCodexHome(args);
5303
+ if (args.cloud && args.agentId && codexHome && !fs.existsSync(codexHome)) {
5304
+ ensureDir(codexHome);
5305
+ info(`已创建云端 Codex 配置目录: ${codexHome}`);
5306
+ }
5295
5307
  if (codexHome) info(`Codex 配置目录: ${codexHome}`);
5296
5308
  written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
5297
5309
  } else {
@@ -5315,6 +5327,7 @@ async function main() {
5315
5327
  summaryLines.push(`Hook script: ${written.hookScript || '~/.codex/hooks/cozeloop_hook.py'}`);
5316
5328
  summaryLines.push(`Config: ${written.hooksJson || '~/.codex/hooks.json'}`);
5317
5329
  summaryLines.push(`Credentials: ${written.envFile || '~/.codex/hooks/cozeloop.env'}`);
5330
+ if (written.logFile) summaryLines.push(`Hook log: ${written.logFile}`);
5318
5331
  } else {
5319
5332
  summaryLines.push(`Config: ~/.openclaw/openclaw.json`);
5320
5333
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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
-
@@ -130,12 +130,15 @@ def _make_finish_event_processor():
130
130
  def _processor(info):
131
131
  try:
132
132
  if not getattr(info, "is_event_fail", False):
133
+ hook_log("upload success")
133
134
  return
134
135
  detail = getattr(info, "detail_msg", "") or ""
135
136
  logid = _extract_logid(detail)
136
137
  if logid:
138
+ hook_log(f"upload failed logid={logid} detail={detail[:500]}")
137
139
  print(f"[CozeLoop] 上报失败 logid={logid} (可用 bytedcli log get-logid-log {logid} 排查)", file=sys.stderr)
138
140
  else:
141
+ hook_log(f"upload failed detail={detail[:500]}")
139
142
  print(f"[CozeLoop] 上报失败: {detail[:300]}", file=sys.stderr)
140
143
  except Exception:
141
144
  pass
@@ -202,23 +205,80 @@ def get_fresh_token():
202
205
  # -------------------------------------------------------------------------
203
206
 
204
207
  # --- SDK Import ---
205
- try:
208
+ def _ensure_cozeloop_sdk():
209
+ try:
210
+ import cozeloop # noqa: F401
211
+ return True
212
+ except ImportError:
213
+ pass
214
+ import subprocess
215
+ import importlib
216
+ import site
217
+ attempts = (
218
+ ["--quiet", "--disable-pip-version-check", "cozeloop"],
219
+ ["--quiet", "--disable-pip-version-check", "--break-system-packages", "cozeloop"],
220
+ ["--quiet", "--disable-pip-version-check", "--break-system-packages", "--user", "cozeloop"],
221
+ )
222
+ for extra in attempts:
223
+ try:
224
+ subprocess.run(
225
+ [sys.executable, "-m", "pip", "install", *extra],
226
+ timeout=180, check=True,
227
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
228
+ )
229
+ except Exception:
230
+ continue
231
+ try:
232
+ importlib.reload(site)
233
+ user_site = site.getusersitepackages()
234
+ for p in ([user_site] if isinstance(user_site, str) else list(user_site)):
235
+ if p and p not in sys.path:
236
+ sys.path.insert(0, p)
237
+ importlib.invalidate_caches()
238
+ import cozeloop # noqa: F401
239
+ print("[CozeLoop] cozeloop SDK auto-installed at runtime.", file=sys.stderr)
240
+ return True
241
+ except ImportError:
242
+ continue
243
+ return False
244
+
245
+
246
+ if _ensure_cozeloop_sdk():
206
247
  import cozeloop
207
248
  from cozeloop.spec.tracespec import (
208
249
  Runtime, ModelInput, ModelMessage, ModelToolChoice,
209
250
  ModelOutput, ModelChoice, ModelToolCall, ModelToolCallFunction,
210
251
  ModelMessagePart, ModelMessagePartType
211
252
  )
212
- except ImportError:
213
- print("Error: cozeloop SDK not found. Please install it with: pip install cozeloop", file=sys.stderr)
253
+ else:
254
+ print("Error: cozeloop SDK not found and auto-install failed. Try: pip install cozeloop", file=sys.stderr)
214
255
  sys.exit(1)
215
256
 
216
257
  # --- Configuration ---
217
258
  DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
218
259
 
219
260
 
261
+ def _log_file_path() -> str:
262
+ return os.environ.get("COZELOOP_HOOK_LOG", "").strip()
263
+
264
+
265
+ def hook_log(message: str):
266
+ """Append one diagnostic line to the hook log, if configured."""
267
+ log_path = _log_file_path()
268
+ if not log_path:
269
+ return
270
+ try:
271
+ p = Path(log_path).expanduser()
272
+ p.parent.mkdir(parents=True, exist_ok=True)
273
+ with p.open("a", encoding="utf-8") as f:
274
+ f.write(f"{datetime.now().isoformat()} {message}\n")
275
+ except Exception:
276
+ pass
277
+
278
+
220
279
  def debug_log(message: str):
221
280
  """Print debug message if debug mode is enabled."""
281
+ hook_log(f"DEBUG {message}")
222
282
  if DEBUG:
223
283
  print(f"[COZELOOP_HOOK_DEBUG] {datetime.now().isoformat()} - {message}", file=sys.stderr)
224
284
 
@@ -652,12 +712,15 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
652
712
  token = get_fresh_token()
653
713
  if token:
654
714
  os.environ["COZELOOP_API_TOKEN"] = token
715
+ hook_log(f"token resolved prefix={token[:12]}...")
655
716
  print(f"[CozeLoop] Token 获取成功 ({token[:12]}...)", file=sys.stderr)
656
717
  else:
718
+ hook_log("token missing")
657
719
  print("[CozeLoop] 警告: 未找到有效 Token,上报可能失败", file=sys.stderr)
658
720
  creds = _load_credentials()
659
721
  workspace_id = (creds or {}).get("workspace_id") or os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
660
722
  os.environ["COZELOOP_WORKSPACE_ID"] = workspace_id
723
+ hook_log(f"workspace_id={workspace_id}")
661
724
  client_kwargs = {
662
725
  "ultra_large_report": True,
663
726
  "upload_timeout": 120,
@@ -980,13 +1043,16 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
980
1043
  debug_log(f"Error processing turn {i}: {e}")
981
1044
  continue
982
1045
 
1046
+ hook_log(f"processed turns={len(turns)} session_id={session_id}")
983
1047
  debug_log(f"Successfully processed {len(turns)} turn(s) for session {session_id}")
984
1048
 
985
1049
  except Exception as e:
1050
+ hook_log(f"send exception={repr(e)}")
986
1051
  debug_log(f"An error occurred while sending traces to CozeLoop: {e}")
987
1052
  return None
988
1053
  finally:
989
1054
  client.close()
1055
+ hook_log("client closed")
990
1056
  debug_log("CozeLoop client closed.")
991
1057
 
992
1058
  return ctx
@@ -997,10 +1063,12 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
997
1063
  def main():
998
1064
  """Main entry point for the Codex CozeLoop hook."""
999
1065
  print("[CozeLoop] Hook triggered (Codex).", file=sys.stderr)
1066
+ hook_log("hook triggered")
1000
1067
  debug_log("Codex CozeLoop hook started.")
1001
1068
 
1002
1069
  # Check if tracing is enabled
1003
1070
  if os.environ.get("TRACE_TO_COZELOOP", "").lower() == "false":
1071
+ hook_log("skip trace disabled")
1004
1072
  debug_log("TRACE_TO_COZELOOP is set to 'false', skipping")
1005
1073
  return
1006
1074
 
@@ -1008,10 +1076,12 @@ def main():
1008
1076
  try:
1009
1077
  raw_input = sys.stdin.read().strip()
1010
1078
  if not raw_input:
1079
+ hook_log("skip no stdin")
1011
1080
  debug_log("No input received from stdin")
1012
1081
  return
1013
1082
  hook_input = json.loads(raw_input)
1014
1083
  except Exception as e:
1084
+ hook_log(f"skip stdin parse error={repr(e)}")
1015
1085
  debug_log(f"Error reading hook input from stdin: {e}")
1016
1086
  return
1017
1087
 
@@ -1020,10 +1090,12 @@ def main():
1020
1090
  # Get transcript path
1021
1091
  transcript_path = hook_input.get("transcript_path")
1022
1092
  if not transcript_path:
1093
+ hook_log("skip missing transcript_path")
1023
1094
  debug_log("No transcript_path in hook input")
1024
1095
  return
1025
1096
 
1026
1097
  if not os.path.exists(transcript_path):
1098
+ hook_log(f"skip transcript not found path={transcript_path}")
1027
1099
  debug_log(f"Transcript file not found: {transcript_path}")
1028
1100
  return
1029
1101
 
@@ -1035,9 +1107,11 @@ def main():
1035
1107
  entries = read_rollout_messages(transcript_path, state["last_processed_line"])
1036
1108
 
1037
1109
  if not entries:
1110
+ hook_log(f"skip no new entries transcript={transcript_path}")
1038
1111
  debug_log("No new entries to process")
1039
1112
  return
1040
1113
 
1114
+ hook_log(f"read entries={len(entries)} from_line={state['last_processed_line']} transcript={transcript_path}")
1041
1115
  debug_log(f"Read {len(entries)} new entries from line {state['last_processed_line']}")
1042
1116
 
1043
1117
  # Parse session identity
@@ -1087,6 +1161,7 @@ def main():
1087
1161
  last_line = max(e.get("_line_number", 0) for e in entries) + 1
1088
1162
  state["last_processed_line"] = last_line
1089
1163
  save_state(state_file, state)
1164
+ hook_log(f"subagent saved session_id={session_id} turns={len(turns[-1:])} last_line={last_line}")
1090
1165
  debug_log("Subagent data saved, hook completed")
1091
1166
  return
1092
1167
 
@@ -1097,6 +1172,7 @@ def main():
1097
1172
  for t in turns
1098
1173
  )
1099
1174
  if not has_coze_ctx:
1175
+ hook_log(f"skip no coze-context turns={len(turns)} session_id={session_id}")
1100
1176
  debug_log("No coze-context found in any turn, skipping upload.")
1101
1177
  return
1102
1178
  history_context = state.get("conversation_history", [])
@@ -1109,15 +1185,18 @@ def main():
1109
1185
  state["last_processed_line"] = last_line
1110
1186
  state["conversation_history"] = updated_history
1111
1187
  save_state(state_file, state)
1188
+ hook_log(f"state advanced last_line={last_line} session_id={session_id}")
1112
1189
  debug_log(f"State updated, last processed line: {last_line}")
1113
1190
  else:
1191
+ hook_log(f"send failed state not advanced session_id={session_id}")
1114
1192
  debug_log("Send failed, state not advanced")
1115
1193
  else:
1194
+ hook_log(f"skip no turns session_id={session_id}")
1116
1195
  debug_log("No turns to send")
1117
1196
 
1197
+ hook_log("hook completed")
1118
1198
  debug_log("Codex CozeLoop hook completed.")
1119
1199
 
1120
1200
 
1121
1201
  if __name__ == "__main__":
1122
1202
  main()
1123
-