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 +11 -0
- package/index.js +105 -46
- package/package.json +7 -2
- package/scripts/claude-code/cozeloop_hook.py +41 -4
- package/scripts/codex/cozeloop_hook.py +41 -4
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
|
|
4627
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5245
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
print("Error: cozeloop SDK not found
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
print("Error: cozeloop SDK not found
|
|
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
|
-
|