@evanbobo/colco 0.0.0-stage → 0.18.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 evanbobo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ @evanbobo/colco
2
+
3
+ The rules for reading Claude Code and Codex session logs (which lines carry usage, how repeated lines,
4
+ mirrored token_count notifications and subagent replays of a parent's history are deduplicated, and how
5
+ each agent's token fields relate to cache and reasoning tokens) follow the documentation and behaviour of
6
+ Tokei (https://github.com/cclank/tokei), published under the MIT License. No Tokei source code is included.
7
+
8
+ The skill folder layout and agent names follow the skills CLI (https://github.com/vercel-labs/skills),
9
+ MIT License.
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # @evanbobo/colco
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Colco (叩叩) collaboration skill for coding agents. It also records which models and how many
4
+ tokens each task used, counted from the agent's own session log on your machine.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx @evanbobo/colco add
10
+ ```
11
+
12
+ The installer lists the supported agents, preselects the ones it finds, and asks:
13
+
14
+ 1. **Agents:** Claude Code (`claude-code`) and Codex (`codex`).
15
+ 2. **Scope:** global, for every project (the default), or only the current project.
16
+ 3. **Method:** link each agent's skills folder to one shared copy (the default), or copy the files.
17
+ 4. **Colco server address**, and an **API key** created on Colco's Agent page (sidebar, Collaboration
18
+ group). The key is checked with `whoami` and saved in `~/.colco/config.json`, readable only by you. If
19
+ `COLCO_AGENT_TOKEN` is set, it is used instead and nothing is saved.
20
+
21
+ Without prompts (CI, scripts, or an agent installing it for you):
22
+
23
+ ```bash
24
+ COLCO_AGENT_TOKEN=colco_… npx @evanbobo/colco add -a claude-code -a codex --server https://colco.example.com -y
25
+ ```
26
+
27
+ | Agent | Skill folder (global) | Skill folder (project) |
28
+ |---|---|---|
29
+ | Claude Code | `~/.claude/skills/colco` (link) | `.claude/skills/colco` |
30
+ | Codex | `~/.agents/skills/colco` (the shared copy) | `.agents/skills/colco` |
31
+
32
+ Restart the agent after installing. `colco list` shows what is installed, and `colco remove` uninstalls it.
33
+
34
+ ## Task usage
35
+
36
+ The skill tells the agent to start and finish tasks with the CLI:
37
+
38
+ ```bash
39
+ colco task start 12 # task 12 → in progress
40
+ colco task done 12 # task 12 → done, then report this session's models and tokens
41
+ colco report --dry-run # preview without sending
42
+ ```
43
+
44
+ How the numbers are counted:
45
+
46
+ 1. **Task boundaries come from the `colco task` commands.** `colco task start` and `colco task done` record
47
+ each moment in `~/.colco/markers.jsonl` (readable only by you), with the agent and its session. A task's
48
+ span runs from its start to its done, or until another task starts. Changing a status anywhere else (the
49
+ web page, `colco call update_task`) does not move the span. Tokens outside any span are reported as
50
+ unassigned and not counted toward a task.
51
+ 2. **Subagents** count toward the task that was active in their parent when they were started. A subagent
52
+ that runs `colco task start` itself counts toward that task from then on: the command prints a marker id,
53
+ and the report finds it in the subagent's own log.
54
+ 3. **Repeated reports never add up.** Each report rescans the whole session, including subagents, and
55
+ replaces what was sent for that task and session before.
56
+ 4. **Nothing is written into the agent's files.** The CLI only reads the agent's logs, so it cannot affect a
57
+ session or its prompt cache.
58
+
59
+ Token fields: `input` excludes cache reads and writes, `output` includes reasoning, and `total` is input,
60
+ output, cache read and cache write added together.
61
+
62
+ ## Finding the project for a code directory
63
+
64
+ ```bash
65
+ colco project # projects that use this directory's repository; * marks the current branch
66
+ colco project --json # the same as JSON
67
+ ```
68
+
69
+ The command reads the directory's git remote (`origin`, or the first remote when there is no origin) and
70
+ the current branch, removes any user name, password or token from the remote, and asks Colco which of your
71
+ projects use that repository. Projects that use the current branch come first. Repositories belong to the
72
+ whole organization, so one repository can serve several projects, each on its own branch. A project owner
73
+ links them on the project's 仓库 tab or with `link_repository`. When nothing matches, the command says so
74
+ and points to `list_projects`.
75
+
76
+ ## Other commands
77
+
78
+ ```bash
79
+ colco whoami # check the connection
80
+ colco call get_task_usage '{"task_id": 12}' # run any Colco tool with the saved key
81
+ ```
82
+
83
+ Requires Node.js 20 or later, and Codex 0.163 or later (earlier versions do not log per-response usage). MIT License; see NOTICE for credits.
package/bin/colco.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.mjs";
3
+
4
+ process.exitCode = await main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@evanbobo/colco",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.18.0",
4
+ "description": "Colco (叩叩) collaboration skill for coding agents, with per-task model and token reporting from local session logs",
5
+ "type": "module",
6
+ "bin": {
7
+ "colco": "bin/colco.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE",
14
+ "NOTICE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "keywords": [
20
+ "colco",
21
+ "agent-skills",
22
+ "claude-code",
23
+ "codex",
24
+ "token-usage"
25
+ ],
26
+ "license": "MIT",
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
30
+ }
31
+ }
@@ -0,0 +1,135 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { homeDir } from "../home.mjs";
4
+ import { count, readJsonl, timeOf } from "../usage/jsonl.mjs";
5
+
6
+ /*
7
+ * Claude Code. A session is `<config>/projects/<project>/<session>.jsonl`, with each subagent in
8
+ * `<session>/subagents/agent-<id>.jsonl` next to `agent-<id>.meta.json`, whose toolUseId names the call that
9
+ * started it. Every assistant line carries `message.usage`; one response is written once per content block,
10
+ * so lines are counted once per message id and request id, and a subagent's copies of its parent's
11
+ * messages are dropped (rules follow tokei's CALCULATION.md). `input_tokens` excludes cache reads and writes;
12
+ * thinking is part of `output_tokens`.
13
+ */
14
+
15
+ /** Claude Code's configuration directory. */
16
+ function configDir(env) {
17
+ return env.CLAUDE_CONFIG_DIR || join(homeDir(env), ".claude");
18
+ }
19
+
20
+ export const claudeCode = {
21
+ name: "claude-code",
22
+ displayName: "Claude Code",
23
+ /** Whether Claude Code is installed for this user. */
24
+ detect: (env = process.env) => existsSync(configDir(env)),
25
+ /** Where its skills live, globally and inside a project. */
26
+ globalSkillsDir: (env = process.env) => join(configDir(env), "skills"),
27
+ projectSkillsDir: ".claude/skills",
28
+ /** The current session's transcript, when this runs inside Claude Code. */
29
+ currentSession(env = process.env) {
30
+ const id = env.CLAUDE_CODE_SESSION_ID;
31
+ return id ? findTranscript(id, env) : null;
32
+ },
33
+ findSession: (id, env = process.env) => findTranscript(id, env),
34
+ /** The session id is the transcript's file name. */
35
+ sessionId: (path) => basename(path, ".jsonl"),
36
+ read: (path) => readClaudeSession(path),
37
+ };
38
+
39
+ /** Finds `<session>.jsonl` under any project folder. */
40
+ function findTranscript(sessionId, env) {
41
+ if (!/^[A-Za-z0-9-]{8,80}$/.test(sessionId)) return null;
42
+ const projects = join(configDir(env), "projects");
43
+ if (!existsSync(projects)) return null;
44
+ for (const project of readdirSync(projects)) {
45
+ const candidate = join(projects, project, `${sessionId}.jsonl`);
46
+ if (existsSync(candidate)) return candidate;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /** Reads one transcript file into a stream (without parent information); `spawns` maps tool calls to their time. */
52
+ function readStream(path, id) {
53
+ const events = [];
54
+ const spawns = new Map();
55
+ for (const row of readJsonl(path)) {
56
+ const at = timeOf(row.timestamp);
57
+ if (at === null) continue;
58
+ const message = row.message && typeof row.message === "object" ? row.message : null;
59
+ const content = Array.isArray(message?.content) ? message.content : [];
60
+ if (row.type === "assistant" && message) {
61
+ for (const block of content) {
62
+ if (block?.type === "tool_use" && typeof block.id === "string") spawns.set(block.id, at);
63
+ }
64
+ const usage = message.usage;
65
+ if (usage && typeof message.model === "string" && message.model !== "<synthetic>") {
66
+ events.push({
67
+ id: `${message.id ?? row.uuid}|${row.requestId ?? ""}`,
68
+ at, model: message.model, sidechain: row.isSidechain === true,
69
+ input: count(usage.input_tokens), output: count(usage.output_tokens),
70
+ cacheRead: count(usage.cache_read_input_tokens), cacheWrite: count(usage.cache_creation_input_tokens), reasoning: 0,
71
+ });
72
+ }
73
+ }
74
+ }
75
+ return { id, path, events: keepLargest(events), spawns };
76
+ }
77
+
78
+ /** Tokens of one event, for choosing among repeated lines. */
79
+ const size = (event) => event.input + event.output + event.cacheRead + event.cacheWrite;
80
+
81
+ /** One event per id; for repeated lines the largest usage wins (it is the most complete), the later on a tie. */
82
+ function keepLargest(events) {
83
+ const byId = new Map();
84
+ for (const event of events) {
85
+ const existing = byId.get(event.id);
86
+ if (!existing || size(event) >= size(existing)) byId.set(event.id, event);
87
+ }
88
+ return [...byId.values()];
89
+ }
90
+
91
+ /** Reads a session and its subagents into streams for attributeSession. */
92
+ export function readClaudeSession(path) {
93
+ const sessionId = basename(path, ".jsonl");
94
+ const main = readStream(path, "main");
95
+ const streams = [main];
96
+ const subagentDir = join(dirname(path), sessionId, "subagents");
97
+ if (existsSync(subagentDir) && statSync(subagentDir).isDirectory()) {
98
+ for (const file of readdirSync(subagentDir).sort()) {
99
+ if (!file.endsWith(".jsonl")) continue;
100
+ const agentId = file.slice(0, -".jsonl".length);
101
+ const stream = readStream(join(subagentDir, file), agentId);
102
+ const meta = readMeta(join(subagentDir, `${agentId}.meta.json`));
103
+ stream.toolUseId = typeof meta?.toolUseId === "string" ? meta.toolUseId : null;
104
+ streams.push(stream);
105
+ }
106
+ }
107
+
108
+ const seen = new Set(main.events.map((event) => event.id));
109
+ for (const stream of streams) {
110
+ if (stream === main) {
111
+ stream.parent = null;
112
+ stream.spawnAt = null;
113
+ continue;
114
+ }
115
+ const owner = streams.find((candidate) => candidate !== stream && stream.toolUseId && candidate.spawns.has(stream.toolUseId));
116
+ stream.parent = owner ? owner.id : "main";
117
+ stream.spawnAt = owner ? owner.spawns.get(stream.toolUseId) : Math.min(...stream.events.map((event) => event.at), Infinity);
118
+ if (!Number.isFinite(stream.spawnAt)) stream.spawnAt = null;
119
+ stream.events = stream.events.filter((event) => {
120
+ if (seen.has(event.id)) return false;
121
+ seen.add(event.id);
122
+ return true;
123
+ });
124
+ }
125
+ return { sessionId, streams: streams.map(({ id, path, parent, spawnAt, events }) => ({ id, path, parent, spawnAt, markers: [], events })) };
126
+ }
127
+
128
+ /** Reads a subagent's meta.json, or null. */
129
+ function readMeta(path) {
130
+ try {
131
+ return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
@@ -0,0 +1,191 @@
1
+ import { closeSync, existsSync, openSync, readdirSync, readSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homeDir } from "../home.mjs";
4
+ import { count, readJsonl, timeOf } from "../usage/jsonl.mjs";
5
+
6
+ /*
7
+ * Codex. A session is `$CODEX_HOME/sessions/YYYY/MM/DD/rollout-<time>-<thread id>.jsonl`; a subagent or fork
8
+ * is its own rollout whose session_meta names the parent thread. Usage comes from `token_usage_record`, one
9
+ * per response, which Codex 0.163 and later write. A child rollout first replays its parent's history, so
10
+ * records owned by another thread are skipped (rules follow tokei's CALCULATION.md). `input_tokens`
11
+ * includes cached input and `output_tokens` includes reasoning.
12
+ */
13
+
14
+ /** Codex's home directory. */
15
+ function codexHome(env) {
16
+ return env.CODEX_HOME || join(homeDir(env), ".codex");
17
+ }
18
+
19
+ /** Folders that hold rollouts, live and archived. */
20
+ function sessionRoots(env) {
21
+ return [join(codexHome(env), "sessions"), join(codexHome(env), "archived_sessions")];
22
+ }
23
+
24
+ export const codex = {
25
+ name: "codex",
26
+ displayName: "Codex",
27
+ /** Whether Codex is installed for this user. */
28
+ detect: (env = process.env) => existsSync(codexHome(env)) || existsSync("/etc/codex"),
29
+ /** Codex reads user skills from ~/.agents/skills and project skills from .agents/skills. */
30
+ globalSkillsDir: (env = process.env) => join(homeDir(env), ".agents", "skills"),
31
+ projectSkillsDir: ".agents/skills",
32
+ /** The current session's rollout, when this runs inside Codex (shell commands get CODEX_THREAD_ID). */
33
+ currentSession(env = process.env) {
34
+ const id = env.CODEX_THREAD_ID;
35
+ return id ? rootRollout(id, sessionRoots(env)) : null;
36
+ },
37
+ findSession: (id, env = process.env) => rootRollout(id, sessionRoots(env)),
38
+ /** The thread id in a rollout's file name. */
39
+ sessionId: (path) => /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(path)?.[1] ?? null,
40
+ read: (path, env = process.env) => readCodexSession(path, { sessionRoots: sessionRoots(env) }),
41
+ };
42
+
43
+ /** Every rollout file under the roots, newest folders first. */
44
+ function listRollouts(roots) {
45
+ const files = [];
46
+ const walk = (dir, depth) => {
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(dir, { withFileTypes: true });
50
+ } catch {
51
+ return;
52
+ }
53
+ for (const entry of entries) {
54
+ const full = join(dir, entry.name);
55
+ if (entry.isDirectory() && depth < 5) walk(full, depth + 1);
56
+ else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) files.push(full);
57
+ }
58
+ };
59
+ for (const root of roots) walk(root, 0);
60
+ return files.sort().reverse();
61
+ }
62
+
63
+ /** Finds the rollout of one thread id. */
64
+ function findRollout(threadId, roots) {
65
+ if (!/^[A-Za-z0-9-]{8,80}$/.test(threadId)) return null;
66
+ return listRollouts(roots).find((file) => file.endsWith(`-${threadId}.jsonl`)) ?? null;
67
+ }
68
+
69
+ /**
70
+ * The rollout of the top thread above `threadId`. A subagent's shell sees its own thread id; reporting
71
+ * from the top thread (which includes every child) keeps one session id per tree, so nothing counts twice.
72
+ */
73
+ function rootRollout(threadId, roots) {
74
+ let path = findRollout(threadId, roots);
75
+ const seen = new Set();
76
+ while (path && !seen.has(path) && seen.size < 1000) {
77
+ seen.add(path);
78
+ const parent = parentOf(headMeta(path)?.meta);
79
+ const parentPath = parent ? findRollout(parent, roots) : null;
80
+ if (!parentPath) break;
81
+ path = parentPath;
82
+ }
83
+ return path;
84
+ }
85
+
86
+ /** The parent thread a session_meta payload names, if any. */
87
+ function parentOf(meta) {
88
+ const spawn = meta?.source?.subagent?.thread_spawn;
89
+ return meta?.forked_from_id || meta?.parent_thread_id || (spawn && typeof spawn === "object" ? spawn.parent_thread_id : null) || null;
90
+ }
91
+
92
+ /** Reads only the session_meta line at the top of a rollout, without loading the whole file. */
93
+ function headMeta(path) {
94
+ let fd;
95
+ try {
96
+ fd = openSync(path, "r");
97
+ const buffer = Buffer.alloc(256 * 1024);
98
+ const length = readSync(fd, buffer, 0, buffer.length, 0);
99
+ for (const line of buffer.subarray(0, length).toString("utf8").split("\n").slice(0, 20)) {
100
+ if (!line.includes('"session_meta"')) continue;
101
+ try {
102
+ const row = JSON.parse(line);
103
+ if (row.type === "session_meta") return { meta: row.payload ?? {}, at: timeOf(row.timestamp) };
104
+ } catch {
105
+ // The meta line can exceed the buffer when it embeds long instructions; then it is unreadable here.
106
+ }
107
+ }
108
+ } catch {
109
+ return null;
110
+ } finally {
111
+ if (fd !== undefined) closeSync(fd);
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /** Usage of one Codex token record, in the shared shape. */
117
+ function usageOf(usage) {
118
+ const input = count(usage.input_tokens);
119
+ const cacheRead = count(usage.cached_input_tokens);
120
+ // cache_write_input_tokens has been 0 in every log seen so far; it is treated as part of input_tokens like cached.
121
+ const cacheWrite = count(usage.cache_write_input_tokens);
122
+ return { input: Math.max(0, input - cacheRead - cacheWrite), output: count(usage.output_tokens), cacheRead, cacheWrite, reasoning: count(usage.reasoning_output_tokens) };
123
+ }
124
+
125
+ /**
126
+ * Reads one rollout into a stream. Codex logs each response as a `token_usage_record` keyed by response_id
127
+ * (it also writes a legacy `token_count` copy, which is ignored). A child rollout first replays its parent's
128
+ * history, whose records name the parent's thread_id, so only this thread's own records count.
129
+ */
130
+ function readStream(path) {
131
+ const rows = readJsonl(path);
132
+ const metaRow = rows.find((row) => row.type === "session_meta");
133
+ const meta = metaRow?.payload ?? {};
134
+ const id = meta.id || meta.session_id || path;
135
+ const events = [];
136
+ const responses = new Set();
137
+ let model = "unknown";
138
+ for (const row of rows) {
139
+ const at = timeOf(row.timestamp);
140
+ const payload = row.payload && typeof row.payload === "object" ? row.payload : {};
141
+ if (at === null) continue;
142
+ if (row.type === "turn_context") {
143
+ model = payload.model || payload.collaboration_mode?.settings?.model || model;
144
+ } else if (row.type === "token_usage_record") {
145
+ if (payload.thread_id && payload.thread_id !== id) continue;
146
+ if (!payload.usage || typeof payload.usage !== "object") continue;
147
+ const key = payload.response_id ? String(payload.response_id) : `${at}|${events.length}`;
148
+ if (responses.has(key)) continue;
149
+ responses.add(key);
150
+ events.push({ id: key, at, model, ...usageOf(payload.usage) });
151
+ }
152
+ }
153
+ return { id, path, parent: parentOf(meta), spawnAt: timeOf(metaRow?.timestamp), markers: [], events };
154
+ }
155
+
156
+ /**
157
+ * Reads a session and every subagent or fork below it into streams for attributeSession. Children are
158
+ * found by scanning rollouts written after the session started for a session_meta naming a known thread.
159
+ */
160
+ export function readCodexSession(path, { sessionRoots: roots }) {
161
+ const main = readStream(path);
162
+ const streams = [{ ...main, parent: null, spawnAt: null }];
163
+ const known = new Set([main.id]);
164
+ const startedAt = main.spawnAt ?? 0;
165
+ const candidates = [];
166
+ for (const file of listRollouts(roots)) {
167
+ if (file === path) continue;
168
+ try {
169
+ if (statSync(file).mtimeMs < startedAt) continue;
170
+ } catch {
171
+ continue;
172
+ }
173
+ const head = headMeta(file);
174
+ const parent = parentOf(head?.meta);
175
+ if (parent) candidates.push({ file, parent });
176
+ }
177
+ let added = true;
178
+ while (added) {
179
+ added = false;
180
+ for (const candidate of candidates) {
181
+ if (candidate.done || !known.has(candidate.parent)) continue;
182
+ candidate.done = true;
183
+ const stream = readStream(candidate.file);
184
+ if (known.has(stream.id)) continue;
185
+ known.add(stream.id);
186
+ streams.push({ ...stream, parent: stream.parent === main.id ? "main" : stream.parent });
187
+ added = true;
188
+ }
189
+ }
190
+ return { sessionId: main.id, streams: streams.map((stream) => (stream.id === main.id ? { ...stream, id: "main" } : stream)) };
191
+ }
@@ -0,0 +1,20 @@
1
+ import { claudeCode } from "./claude-code.mjs";
2
+ import { codex } from "./codex.mjs";
3
+
4
+ /*
5
+ * Supported agents, one module each. An agent module says how to detect the agent, where its skills live
6
+ * and, when it can report usage, how to find the current session and read its log. Names follow the
7
+ * skills CLI (`npx skills add -a <name>`), so the same flags work for both. Adding an agent means adding
8
+ * one module here.
9
+ */
10
+ export const AGENTS = [claudeCode, codex];
11
+
12
+ /** The agent with this name, or undefined. */
13
+ export function agentByName(name) {
14
+ return AGENTS.find((agent) => agent.name === name);
15
+ }
16
+
17
+ /** The agent this process runs inside, judged by the session variables agents give their shell commands. */
18
+ export function currentAgent(env = process.env) {
19
+ return AGENTS.find((agent) => agent.currentSession && agent.currentSession(env) !== null);
20
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ /*
4
+ * The Colco REST transport: `POST <server>/api/agent/tools/<tool>` with a bearer API key. Tool results
5
+ * contain text written by people, so callers treat them as data.
6
+ */
7
+
8
+ export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
9
+
10
+ export class ColcoError extends Error {
11
+ constructor(message, status = 0, code = "") {
12
+ super(message);
13
+ this.name = "ColcoError";
14
+ this.status = status;
15
+ this.code = code;
16
+ }
17
+ }
18
+
19
+ /** Calls one tool and returns its data, or throws ColcoError with the server's message. */
20
+ export async function callTool({ server, token }, name, args = {}, fetchImpl = fetch) {
21
+ if (!server) throw new ColcoError("还没有配置 Colco 服务地址:运行 colco add,或设置 COLCO_SERVER");
22
+ if (!token) throw new ColcoError("还没有 API Key:运行 colco add,或设置 COLCO_AGENT_TOKEN");
23
+ let response;
24
+ try {
25
+ response = await fetchImpl(`${server}/api/agent/tools/${encodeURIComponent(name)}`, {
26
+ method: "POST",
27
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "X-Agent-Client": `colco-cli/${VERSION}` },
28
+ body: JSON.stringify(args),
29
+ });
30
+ } catch (error) {
31
+ throw new ColcoError(`连接 Colco 失败:${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ const body = await response.json().catch(() => null);
34
+ if (!response.ok || !body || body.ok !== true) {
35
+ const message = body?.error?.message || `Colco 返回 HTTP ${response.status}`;
36
+ throw new ColcoError(message, response.status, body?.error?.code || "");
37
+ }
38
+ return body.data;
39
+ }