@evanbobo/colco 0.12.0 → 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/README.md CHANGED
@@ -59,6 +59,20 @@ How the numbers are counted:
59
59
  Token fields: `input` excludes cache reads and writes, `output` includes reasoning, and `total` is input,
60
60
  output, cache read and cache write added together.
61
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
+
62
76
  ## Other commands
63
77
 
64
78
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanbobo/colco",
3
- "version": "0.12.0",
3
+ "version": "0.18.0",
4
4
  "description": "Colco (叩叩) collaboration skill for coding agents, with per-task model and token reporting from local session logs",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -5,6 +5,7 @@ import { callTool, ColcoError, VERSION } from "./api.mjs";
5
5
  import { configPath, normaliseServer, readConfig, resolveConnection, TOKEN_ENV, writeConfig } from "./config.mjs";
6
6
  import { homeDir } from "./home.mjs";
7
7
  import { applyInstall, applyRemove, inspect, planInstall } from "./install.mjs";
8
+ import { project, runGit } from "./project.mjs";
8
9
  import { cliConfirmation, readMarkers, recordMarker, TASK_STATUSES } from "./usage/markers.mjs";
9
10
  import { buildReports, sendReports, totalTokens } from "./usage/report.mjs";
10
11
  import { ask, askSecret, choose, multiSelect } from "./prompt.mjs";
@@ -13,7 +14,7 @@ import { ask, askSecret, choose, multiSelect } from "./prompt.mjs";
13
14
  * The colco command. `add`, `list` and `remove` manage the skill for the chosen agents; `task start|done`
14
15
  * move a task, record where it starts or ends in Colco's marker file and, on done, report the session's
15
16
  * usage; `report` reports (or previews) without moving anything; `call` runs any Colco tool with the saved
16
- * API key.
17
+ * API key; `project` finds the projects that use this directory's git repository.
17
18
  */
18
19
 
19
20
  const HELP = `colco ${VERSION} — Colco(叩叩)协作的 Agent Skill 与用量上报
@@ -30,6 +31,7 @@ const HELP = `colco ${VERSION} — Colco(叩叩)协作的 Agent Skill 与用
30
31
  colco report [--task <id>] [--dry-run] [--agent <name> --session <id>]
31
32
  重新统计本会话并上报;--dry-run 只预览
32
33
  colco call <tool> ['<json>'] 用已保存的 API Key 调用任意 Colco 工具
34
+ colco project [--json] 按当前目录的 git 远程地址和分支找到对应的 Colco 项目
33
35
  colco whoami 检查连接
34
36
 
35
37
  选项:
@@ -38,6 +40,7 @@ const HELP = `colco ${VERSION} — Colco(叩叩)协作的 Agent Skill 与用
38
40
  --copy 复制文件而不是创建链接
39
41
  -y, --yes 不提问,使用默认值
40
42
  --server <url> Colco 服务地址(也可设置 COLCO_SERVER)
43
+ --json colco project 以 JSON 输出
41
44
  API Key 读取 ${TOKEN_ENV},或 colco add 时保存在 ~/.colco/config.json(仅本人可读)。`;
42
45
 
43
46
  /** Splits argv into positionals and flags; repeated flags collect into arrays. */
@@ -45,7 +48,7 @@ export function parseArgs(argv) {
45
48
  const positionals = [];
46
49
  const flags = { agent: [] };
47
50
  const valued = new Map([["-a", "agent"], ["--agent", "agent"], ["--server", "server"], ["--task", "task"], ["--session", "session"], ["--status", "status"]]);
48
- const booleans = new Map([["-p", "project"], ["--project", "project"], ["-g", "global"], ["--global", "global"], ["--copy", "copy"], ["-y", "yes"], ["--yes", "yes"], ["--force", "force"], ["--dry-run", "dryRun"], ["-h", "help"], ["--help", "help"], ["-v", "version"], ["--version", "version"]]);
51
+ const booleans = new Map([["-p", "project"], ["--project", "project"], ["-g", "global"], ["--global", "global"], ["--copy", "copy"], ["-y", "yes"], ["--yes", "yes"], ["--force", "force"], ["--dry-run", "dryRun"], ["--json", "json"], ["-h", "help"], ["--help", "help"], ["-v", "version"], ["--version", "version"]]);
49
52
  for (let index = 0; index < argv.length; index += 1) {
50
53
  const arg = argv[index];
51
54
  const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
@@ -236,12 +239,13 @@ async function task(positionals, flags, io) {
236
239
  return report(flags, io, { found });
237
240
  }
238
241
 
239
- /** Runs the CLI; returns the exit code. `io` carries output and the fetch used for every request. */
242
+ /** Runs the CLI; returns the exit code. `io` carries output, the fetch used for every request and the git runner. */
240
243
  export async function main(argv, output = {}) {
241
244
  const io = {
242
245
  out: (line) => process.stdout.write(`${line}\n`),
243
246
  err: (line) => process.stderr.write(`${line}\n`),
244
247
  fetch: (...args) => fetch(...args),
248
+ git: runGit,
245
249
  ...output,
246
250
  };
247
251
  try {
@@ -268,6 +272,8 @@ export async function main(argv, output = {}) {
268
272
  return await task(rest, flags, io);
269
273
  case "report":
270
274
  return await report(flags, io, { onlyTask: flags.task === undefined ? null : taskIdOf(flags.task) });
275
+ case "project":
276
+ return await project(flags, io, resolveConnection());
271
277
  case "whoami": {
272
278
  const me = await callTool(resolveConnection(), "whoami", {}, io.fetch);
273
279
  io.out(`${me.displayName || me.username}(${me.username})`);
@@ -0,0 +1,78 @@
1
+ import { execFile } from "node:child_process";
2
+ import { callTool, ColcoError } from "./api.mjs";
3
+
4
+ /*
5
+ * `colco project`: which Colco projects use the code in this directory. It reads the git remote (origin,
6
+ * else the first remote) and the current branch, and asks Colco's find_projects_by_repository. Git runs
7
+ * through execFile with an argument list, never a shell, so nothing in a remote or branch name is executed.
8
+ */
9
+
10
+ const NOT_A_REPOSITORY = "当前目录不是 git 仓库:请在代码目录中运行 colco project";
11
+
12
+ /** Runs git with `args` in the current directory; resolves trimmed stdout, rejects with git's error. */
13
+ export function runGit(args) {
14
+ return new Promise((resolve, reject) => {
15
+ execFile("git", args, { encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout) => {
16
+ if (error) reject(error);
17
+ else resolve(String(stdout).trim());
18
+ });
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Removes `user:password@`, query and fragment from a URL-style remote, so no credential leaves the machine or
24
+ * reaches the screen. scp-style remotes (`git@host:path`) are left as they are; their user is not a secret.
25
+ */
26
+ export function stripCredentials(remote) {
27
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) return remote;
28
+ return remote.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, "$1").replace(/[?#].*$/, "");
29
+ }
30
+
31
+ /** The directory's remote URL and current branch (null when HEAD is detached or has no commit yet). */
32
+ export async function readGitContext(git) {
33
+ let inside;
34
+ try {
35
+ inside = await git(["rev-parse", "--is-inside-work-tree"]);
36
+ } catch (error) {
37
+ if (error?.code === "ENOENT") throw new ColcoError("没有找到 git 命令:请先安装 git");
38
+ throw new ColcoError(NOT_A_REPOSITORY);
39
+ }
40
+ if (inside !== "true") throw new ColcoError(NOT_A_REPOSITORY);
41
+ let remote = await git(["remote", "get-url", "origin"]).catch(() => "");
42
+ if (!remote) {
43
+ const [first] = (await git(["remote"]).catch(() => "")).split("\n").map((name) => name.trim()).filter(Boolean);
44
+ if (first) remote = await git(["remote", "get-url", "--", first]).catch(() => "");
45
+ }
46
+ if (!remote) throw new ColcoError("这个 git 仓库还没有远程地址(git remote),Colco 无法据此找到项目");
47
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "");
48
+ return { remote: stripCredentials(remote), branch: head && head !== "HEAD" ? head : null };
49
+ }
50
+
51
+ /** Prints the projects that use this directory's repository, or why there are none; `--json` prints the data. */
52
+ export async function project(flags, io, connection) {
53
+ const { remote, branch } = await readGitContext(io.git);
54
+ const data = await callTool(connection, "find_projects_by_repository", { url: remote, ...(branch ? { branch } : {}) }, io.fetch);
55
+ if (flags.json) {
56
+ io.out(JSON.stringify({ remote, branch, ...data }, null, 2));
57
+ return 0;
58
+ }
59
+ io.out(`仓库:${data.repository?.url ?? remote}${branch ? `(当前分支 ${branch})` : ""}`);
60
+ if (!data.repository) {
61
+ io.out("这个仓库还没有登记到 Colco,没有项目使用它。");
62
+ io.out("用 colco call list_projects 查看你的项目;项目负责人可以用 link_repository 把仓库关联到项目。");
63
+ return 0;
64
+ }
65
+ if (data.projects.length === 0) {
66
+ io.out(`仓库「${data.repository.name}」已登记,但你能访问的项目都没有使用它。`);
67
+ io.out("用 colco call list_projects 查看你的项目。");
68
+ return 0;
69
+ }
70
+ const idWidth = Math.max(...data.projects.map((item) => String(item.projectId).length));
71
+ const nameWidth = Math.max(...data.projects.map((item) => item.projectName.length));
72
+ for (const item of data.projects) {
73
+ const archived = item.projectStatus === "archived" ? "(已归档)" : "";
74
+ io.out(`${item.branchMatched ? "*" : " "} ${String(item.projectId).padStart(idWidth)} ${item.projectName.padEnd(nameWidth)} ${item.branch}${archived}`);
75
+ }
76
+ if (data.projects.some((item) => item.branchMatched)) io.out("* 项目使用的就是当前分支");
77
+ return 0;
78
+ }