@iamlbccc/tdxd 1.1.1 → 1.2.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.
@@ -0,0 +1,44 @@
1
+ # TDX-Daemon 接入卡(Prompt 模板)
2
+
3
+ > 用途:平台操作员在 Paperclip UI 生成"邀请 Agent"后,把下面的提示词发给任意 Host 上的
4
+ > AI Agent(如 opencode/Claude Code 等,具备 shell 执行能力即可),Agent 自助完成接入。
5
+ > 使用方法:替换 `<INVITE_URL>` 与 `<WORKSPACE>` 两个占位符后整段发送。
6
+
7
+ ---
8
+
9
+ 你是运行在本机的 AI Agent。请自助接入 Paperclip 平台,成为组织中的远程成员。
10
+
11
+ ## 执行步骤
12
+
13
+ 1. 运行接入命令(会自动完成:创建实例 → 启动 daemon → 提交入职 → 等待批准 → 领取 API key):
14
+
15
+ ```bash
16
+ npx @tdx/tdxd@latest onboard \
17
+ --invite <INVITE_URL> \
18
+ --workspace <WORKSPACE> \
19
+ --capabilities "<用一句话概括你的能力>"
20
+ ```
21
+
22
+ - `<INVITE_URL>`:邀请文档地址(形如 `http://<平台>:3100/api/invites/pcp_invite_xxx/onboarding.txt`)
23
+ - `<WORKSPACE>`:你的工作目录(Agent 干活时的 repo 根目录)
24
+ - 前置条件:Node.js ≥ 24、本机已安装 opencode
25
+
26
+ 2. 命令会在「4/4 等待 board 批准」处轮询暂停。此时**提醒你的用户**:
27
+ "已提交入职申请,请到 Paperclip 平台(组织 → 入职/Join 请求)批准我的加入"。
28
+ 批准后命令自动继续并完成 key 领取。
29
+
30
+ 3. 看到 `✓ READY <实例名> health=200 回调:启用` 即接入完成。
31
+ 向用户报告实例名与 endpoint,并提示:在平台向我派发一个测试任务即可验收。
32
+
33
+ 4. 执行前自查:`node --version`(≥24)、`command -v opencode`;缺失先报告用户再继续。
34
+
35
+ ## 失败处置
36
+
37
+ - 命令中途退出(如批准超时):状态已保存,用户批准后**重跑同一条命令**即断点续接
38
+ - `claimSecret 已过期`:请用户重新生成邀请,再重跑
39
+ - 平台不可达:报告用户检查网络或让平台执行 `npx paperclipai allowed-hostname <本机可达平台的主机名>`
40
+
41
+ ## 纪律
42
+
43
+ - 不要打印、记录或转述任何完整 token / key(命令输出只含掩码,明文仅写入 secret.env)
44
+ - 接入完成后你的 API key 已由 daemon 托管,无需手动管理
package/README.md CHANGED
@@ -10,15 +10,26 @@ npm install -g @iamlbccc/tdxd # Node.js >= 24
10
10
 
11
11
  ## 快速开始
12
12
 
13
+ ### 方式一:邀请制自助接入(推荐,AI 执行)
14
+
15
+ 平台 UI 生成邀请 → 把 [ONBOARD-PROMPT.md](./ONBOARD-PROMPT.md) 的接入卡发给 Host 上的 AI Agent,一条命令完成:
16
+
17
+ ```bash
18
+ npx @tdx/tdxd@latest onboard --invite <INVITE_URL> --workspace .
19
+ # init → start → join → 等批准(轮询) → claim key 自动回填 → READY
20
+ ```
21
+
22
+ ### 方式二:手工分步(人执行)
23
+
13
24
  ```bash
14
- # 1. 初始化一个 Agent 实例(交互式向导:config + token + workspace 骨架)
15
- tdxd-ctl init my-agent
25
+ # 1. 初始化(纯 flags,skills 默认装配:paperclip 心跳 + converting-plans 分派)
26
+ tdxd-ctl init my-agent --workspace ~/workspaces/my-agent
16
27
 
17
28
  # 2. 启动并自检(health=200 即就绪)
18
- tdxd-ctl start my-agent
19
- tdxd-ctl status my-agent
29
+ tdxd-ctl start my-agent && tdxd-ctl status my-agent
20
30
 
21
- # 3. 在 Paperclip 平台 UI hire Agent(选 TDX-Adapter),粘贴 init 打印的 endpoint/token
31
+ # 3. 平台 UI hireTDX-Adapter),粘贴 init 打印的 endpoint/token
32
+ # 4. 铸 agent key 回填:tdxd-ctl key my-agent --set pcp_xxx && tdxd-ctl restart my-agent
22
33
  ```
23
34
 
24
35
  实例布局(agent-first,每 Agent 一套内聚目录):
package/lib/tpl.mjs CHANGED
@@ -1,6 +1,7 @@
1
- // init 脚手架模板:实例 config / workspace 骨架(AGENTS.md 角色模板 + opencode.json
1
+ // init 脚手架模板:实例 config(不生成任何 workspace 文件——daemon 帮现存 Agent 接入,不制造 Agent
2
+ // 模型/价目一律沿用 workspace 自身配置,不在实例 config 里覆盖或占位
2
3
 
3
- export function configTpl({ name, displayName, port, workspace, model, url }) {
4
+ export function configTpl({ displayName, port, workspace, url }) {
4
5
  return `${JSON.stringify({
5
6
  name: displayName,
6
7
  port,
@@ -22,56 +23,19 @@ export function configTpl({ name, displayName, port, workspace, model, url }) {
22
23
  { command: "^(jq|echo|cd|sleep|mkdir +-p +/tmp/opencode)\\b" },
23
24
  ],
24
25
  },
25
- pricing: {
26
- models: {
27
- [model]: {
28
- inputPerMillion: 0.6,
29
- cachedReadPerMillion: 0.15,
30
- outputPerMillion: 2.5,
31
- _doc: "占位价(USD/百万token):请按模型官方定价校准",
32
- },
33
- },
34
- },
35
26
  }, null, 2)}\n`;
36
27
  }
37
28
 
38
- export function opencodeJsonTpl(model) {
39
- return `${JSON.stringify({ $schema: "https://opencode.ai/config.json", model }, null, 2)}\n`;
29
+ /** workspace 既有 opencode 配置探测模型(.opencode/opencode.json 或 opencode.json),探测不到返回 null */
30
+ export function detectModel(workspace) {
31
+ for (const p of [".opencode/opencode.json", "opencode.json"]) {
32
+ try {
33
+ const j = JSON.parse(fs.readFileSync(path.join(workspace, p), "utf8"));
34
+ if (typeof j.model === "string" && j.model) return j.model;
35
+ } catch { /* 不存在或非法 → 下一处 */ }
36
+ }
37
+ return null;
40
38
  }
41
39
 
42
- const PERSONAS = {
43
- manager: `# {NAME}
44
-
45
- 你是团队负责人(Manager)。收到任务时:
46
-
47
- - 先判断:亲自做、拆分给合适成员、还是需要澄清——不确定就到任务里提问,不要猜
48
- - 拆分时建子任务(子任务写清背景/验收标准/依赖),设置好阻塞关系再分派
49
- - 跟踪进度:关注子任务状态,全部完成后汇总收尾、更新父任务
50
- - 输出简洁:状态行 + 要点列表,明确"已完成/待办/谁负责"
51
- `,
52
- coder: `# {NAME}
53
-
54
- 你是开发工程师。收到任务时:
55
-
56
- - 先读相关代码再动手;改动遵循仓库现有风格与约定
57
- - 改完必须验证(跑最窄的相关测试/编译),验证不了要明说
58
- - 提交信息简洁;在任务评论里留下:改了什么、为什么、如何验证
59
- - 发现超出本任务的问题:记录,不擅自扩大改动面
60
- `,
61
- qa: `# {NAME}
62
-
63
- 你是测试工程师。收到任务时:
64
-
65
- - 明确被测对象与验收标准;先设计用例覆盖正常/边界/异常路径
66
- - 如实报告:通过/失败逐条列出,失败附复现步骤与期望结果
67
- - 不掩盖问题,不为了"通过"放宽标准
68
- `,
69
- blank: `# {NAME}
70
-
71
- (在此描述该 Agent 的角色职责与工作规范。)
72
- `,
73
- };
74
-
75
- export function agentsMdTpl(persona, name) {
76
- return (PERSONAS[persona] ?? PERSONAS.blank).replaceAll("{NAME}", name);
77
- }
40
+ import fs from "node:fs";
41
+ import path from "node:path";
package/lib/ui.mjs CHANGED
@@ -1,102 +1,56 @@
1
- // 零依赖交互式提示(跨平台):ask / confirm / select
2
- // TTY:方向键选择;非 TTY(管道/CI):自动降级为编号输入
3
- import readline from "node:readline";
4
- import readlinePromises from "node:readline/promises";
5
- import { once } from "node:events";
1
+ // 呈现层(非 TTY 自动去色,尊重管道)
2
+ // 注:无交互提示——ctl 面向 AI 调用/脚本化,输入一律走命令行 flags
6
3
  import process from "node:process";
7
4
 
8
5
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
9
- const C = { cyan: (s) => `\x1b[36m${s}\x1b[0m`, green: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m` };
10
6
 
11
7
  export function out(s = "") { process.stdout.write(s + "\n"); }
12
-
13
- // TTY:共享单个 readline 逐问读取
14
- let sharedRl = null;
15
- function getRl() {
16
- return sharedRl ??= readlinePromises.createInterface({ input: process.stdin, output: process.stdout });
17
- }
18
- export function closePrompt() { sharedRl?.close(); sharedRl = null; }
19
-
20
- // 非TTY(管道/CI):readline 管道模式在首个应答后会暂停输入流,顺序提问必挂;
21
- // 改为启动即挂常驻 line 监听,一次性收完全部输入,提问从队列按序取
22
- let lineQueue = null;
23
- let queueReady = null;
24
- function warmupNonTty() {
25
- if (!queueReady) {
26
- const lines = [];
27
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
28
- rl.on("line", (l) => lines.push(l));
29
- queueReady = once(rl, "close").then(() => { lineQueue = lines; });
30
- }
31
- return queueReady;
32
- }
33
-
34
- async function question(prompt) {
35
- if (!isTTY) {
36
- process.stdout.write(prompt);
37
- await warmupNonTty();
38
- return (lineQueue.shift() ?? "").trim();
39
- }
40
- const rl = getRl();
41
- try { return ((await rl.question(prompt)) ?? "").trim(); }
42
- catch { return ""; }
8
+ export function closePrompt() { /* 兼容占位(已无交互) */ }
9
+
10
+ const ANSI = {
11
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
12
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
13
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
14
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
15
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
16
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
17
+ };
18
+ function paint(s, kind) { return isTTY && ANSI[kind] ? ANSI[kind](s) : s; }
19
+
20
+ export function ok(s) { out(`${paint("✓", "green")} ${s}`); }
21
+ export function fail(s) { out(`${paint("✗", "red")} ${s}`); }
22
+ export function warn(s) { out(`${paint("⚠", "yellow")} ${s}`); }
23
+ export function head(s) { out(paint(s, "bold")); }
24
+ export function note(s) { out(paint(s, "dim")); }
25
+ /** 行内局部着色(与非 TTY 自动降级一致) */
26
+ export const tGreen = (s) => paint(s, "green");
27
+ export const tRed = (s) => paint(s, "red");
28
+ export const tYellow = (s) => paint(s, "yellow");
29
+ export const tDim = (s) => paint(s, "dim");
30
+ export const tCyan = (s) => paint(s, "cyan");
31
+ export const tBold = (s) => paint(s, "bold");
32
+ export const cell = (text, color) => ({ text: String(text), color });
33
+
34
+ /** CJK 感知的显示宽度(中文按 2 列计) */
35
+ export function dispWidth(s) {
36
+ let w = 0;
37
+ for (const ch of String(s)) w += /[\u1100-\u115F\u2E80-\u303F\u3041-\u33FF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/.test(ch) ? 2 : 1;
38
+ return w;
43
39
  }
44
-
45
- export async function ask(message, def = "") {
46
- const suffix = def !== "" && def != null ? C.dim(` (${def})`) : "";
47
- const v = await question(`${C.cyan("?")} ${message}${suffix}: `);
48
- return v === "" ? String(def ?? "") : v;
49
- }
50
-
51
- export async function confirm(message, def = true) {
52
- const hint = def ? "Y/n" : "y/N";
53
- const v = (await question(`${C.cyan("?")} ${message} ${C.dim(`(${hint})`)}: `)).toLowerCase();
54
- if (v === "") return def;
55
- return v.startsWith("y");
56
- }
57
-
58
- /** 选项选择:TTY ↑↓+回车;非 TTY 用编号。返回 { value, label } */
59
- export async function select(message, options, defIndex = 0) {
60
- if (!isTTY) {
61
- out(`${C.cyan("?")} ${message}`);
62
- options.forEach((o, i) => out(` ${C.bold(i + 1)}. ${o.label}${o.description ? C.dim(` — ${o.description}`) : ""}`));
63
- const v = await question(` 输入编号 ${C.dim(`[1-${options.length}, 默认 ${defIndex + 1}]`)}: `);
64
- const i = Number(v) - 1;
65
- return options[Number.isInteger(i) && i >= 0 && i < options.length ? i : defIndex];
66
- }
67
- return new Promise((resolve) => {
68
- let idx = defIndex;
69
- const render = () => {
70
- readline.cursorTo(process.stdout, 0, 0);
71
- readline.clearScreenDown(process.stdout);
72
- out(`${C.cyan("?")} ${message} ${C.dim("(↑↓ 选择, 回车确认)")}`);
73
- options.forEach((o, i) => out(` ${i === idx ? C.green("❯") : " "} ${i === idx ? C.green(o.label) : o.label}${o.description ? C.dim(` — ${o.description}`) : ""}`));
74
- };
75
- readline.emitKeypressEvents(process.stdin);
76
- process.stdin.setRawMode(true);
77
- const rawCols = process.stdout.columns;
78
- process.stdout.rows = options.length + 2;
79
- if (!rawCols) process.stdout.columns = 80;
80
- render();
81
- process.stdin.resume();
82
- const onData = (_, key) => {
83
- if (key.name === "up") { idx = (idx - 1 + options.length) % options.length; render(); }
84
- else if (key.name === "down") { idx = (idx + 1) % options.length; render(); }
85
- else if (key.name === "return" || key.name === "space") {
86
- cleanup(); resolve(options[idx]);
87
- } else if (key.name === "c" && key.ctrl) {
88
- cleanup(); process.exit(130);
89
- } else if (key.sequence === "3") { /* 忽略 */ }
90
- };
91
- const origCols = rawCols;
92
- function cleanup() {
93
- process.stdin.removeListener("keypress", onData);
94
- process.stdin.setRawMode(false);
95
- process.stdin.pause();
96
- process.stdout.columns = origCols;
97
- readline.cursorTo(process.stdout, 0, options.length + 1);
98
- out(C.dim(` 已选: ${options[idx].label}`));
99
- }
100
- process.stdin.on("keypress", onData);
101
- });
40
+ function padCell(s, width) { return s + " ".repeat(Math.max(0, width - dispWidth(s))); }
41
+
42
+ /** 对齐表格:headers: string[], rows: (string|{text,color})[][]。先填充后着色,末列不补空格 */
43
+ export function table(headers, rows) {
44
+ const raw = (c) => (typeof c === "object" && c ? c.text : c);
45
+ const col = (c) => (typeof c === "object" && c ? c.color : undefined);
46
+ const all = [headers, ...rows];
47
+ const widths = headers.map((_, i) => Math.max(...all.map((r) => dispWidth(raw(r[i]) ?? ""))));
48
+ const last = headers.length - 1;
49
+ const render = (r) => r.map((c, i) => {
50
+ const padded = i === last ? raw(c) ?? "" : padCell(raw(c) ?? "", widths[i]);
51
+ return paint(padded, col(c));
52
+ }).join(" ");
53
+ out(render(headers.map((h) => cell(h, "dim"))));
54
+ out(widths.map((w, i) => paint(i === last ? "" : "─".repeat(w), "dim")).join(" ").trimEnd());
55
+ for (const r of rows) out(render(r));
102
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamlbccc/tdxd",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "TDX-Daemon — remote daemon for Paperclip TDX-Adapter: drives ACP agents (opencode) with session-per-process, permission approval loop, usage metering; ships cross-platform tdxd-ctl (init/scaffold/start/stop/status)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,10 +12,12 @@
12
12
  "tdxd-ctl": "./tdxd-ctl.mjs"
13
13
  },
14
14
  "files": [
15
+ "skills/",
15
16
  "tdxd.mjs",
16
17
  "tdxd-ctl.mjs",
17
18
  "lib/",
18
19
  "config.example.json",
20
+ "ONBOARD-PROMPT.md",
19
21
  "README.md"
20
22
  ],
21
23
  "keywords": [