@abelxiaoxing/cadence 1.8.1 → 1.8.2

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
@@ -14,6 +14,33 @@
14
14
  4. 首次进入项目先运行 `/abel-init`,然后 `/abel-design <需求>`,确认集中呈现的目标与方案,设计完成后运行 `/abel-implement <change>`。
15
15
  5. 暂停时先查看原因和建议,不要靠反复 resume 或重启“碰运气”;未完成的工作会保留。
16
16
 
17
+ ### 每次加载时的配置提示卡
18
+
19
+ 每次扩展启动、重载或恢复会话,Cadence 都会在输入框上方显示一张简短研究能力提示卡,不需要额外记住配置或诊断命令。
20
+ 卡片只检查本地配置,显示 Context7、Grok 与可选 Tavily 的配置状态及当前生效文件;“已配置”不代表网络、认证或服务能力已验证。
21
+ 开始处理任务后卡片收起,下次加载重新显示;不记录“已看过”或“暂时不用”来抑制后续提示。
22
+ 安装 Cadence 时会创建用户级模板 `~/.pi/agent/cadence/.env`(已有文件不会覆盖);卡片本身不会联网、调用模型、写入配置、索要密钥或自动激活 Abel 工作流,可选服务未配置不阻塞普通任务。
23
+ 未信任的项目只显示未检查提示,不读取研究配置。
24
+
25
+ 安装后用户级模板已包含 Grok 配置占位字段;Context7 无配置文件时仍使用默认地址匿名访问,显式配置文件损坏仍会报错。
26
+ 联网搜索需要在本地文件中配置以下内容,每项一行(替换为实际服务地址和密钥):
27
+
28
+ ```dotenv
29
+ GROK_API_URL=https://api.x.ai/v1
30
+ GROK_API_KEY=<your-api-key>
31
+ ```
32
+
33
+ 安装器会创建用户级 `~/.pi/agent/cadence/.env` 及其父目录,并且只在文件不存在时写入模板;也可手动创建该文件以跨项目复用。
34
+ 如果项目已有 `.pi/cadence/.env`,它按整文件优先,**不会与用户级文件合并**;请编辑卡片显示的生效文件,不要覆盖已有无关配置。
35
+ 研究 Skills 不读取 shell 中的 API 环境变量,也不读取安装包旁的 `.env`;保存后下次 Skill 调用生效,无需重启。
36
+ 可选 `CONTEXT7_API_KEY` 提供认证访问;可选 `TAVILY_API_KEY` 启用扩展检索、抓取和站点映射,`TAVILY_ENABLED=false` 可关闭它。
37
+ 完整字段见 [配置模板](config/.env.example),配置文件限普通文件且不超过 64 KiB。
38
+ 密钥只在本地编辑,勿发送到聊天或提交 Git;使用项目级文件时应将其加入忽略规则。
39
+
40
+ TUI 显示卡片;RPC 通过非阻塞 `setWidget` 事件发送同一卡片,Web UI 需实现该事件的展示。
41
+ Print / JSON 无交互界面,不额外输出卡片、不等待输入,也不污染标准输出;可按上述路径直接配置。
42
+ 本版本提供提示与本地编辑指引,不提供密钥输入向导或自动重试搜索。
43
+
17
44
  ### Pi / Web UI 阶段启动约定
18
45
 
19
46
  阶段命令需要 Cadence 扩展与包内 prompts 同时加载;只有提示正文可见,不代表 `abel_dispatch` 已注册或已激活。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,6 +37,7 @@
37
37
  ]
38
38
  },
39
39
  "scripts": {
40
+ "postinstall": "node src/install-config.mjs",
40
41
  "pack:check": "node scripts/pack-check.mjs",
41
42
  "traceability:check": "node scripts/traceability-check.mjs",
42
43
  "verify": "bun run check && bun run lint && bun run test && bun run pack:check",
@@ -1,7 +1,13 @@
1
1
  export declare function parseEnvFile(content: string): Record<string, string>;
2
2
 
3
+ export declare function selectConfigPath(options: {
4
+ cwd: string;
5
+ home: string;
6
+ }): string | null;
7
+
3
8
  export declare function loadConfig(options: {
4
9
  cwd: string;
5
10
  home: string;
6
11
  required?: readonly string[];
7
- }): { path: string; values: Record<string, string> };
12
+ allowMissing?: boolean;
13
+ }): { path: string | null; values: Record<string, string> };
@@ -1,4 +1,11 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import {
2
+ closeSync,
3
+ constants,
4
+ fstatSync,
5
+ lstatSync,
6
+ openSync,
7
+ readSync,
8
+ } from "node:fs";
2
9
  import path from "node:path";
3
10
 
4
11
  export function parseEnvFile(content) {
@@ -37,22 +44,68 @@ export function parseEnvFile(content) {
37
44
  return values;
38
45
  }
39
46
 
40
- export function loadConfig({ cwd, home, required = [] }) {
47
+ function pathEntryExists(filePath) {
48
+ try {
49
+ // lstat, rather than existsSync, intentionally retains dangling symlinks.
50
+ // They are an explicit configuration path and must fail closed when opened.
51
+ lstatSync(filePath);
52
+ return true;
53
+ } catch (error) {
54
+ if (error?.code === "ENOENT") return false;
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ export function selectConfigPath({ cwd, home }) {
41
60
  const projectPath = path.join(cwd, ".pi", "cadence", ".env");
42
61
  const userPath = path.join(home, ".pi", "agent", "cadence", ".env");
43
- const selectedPath = existsSync(projectPath)
62
+ return pathEntryExists(projectPath)
44
63
  ? projectPath
45
- : existsSync(userPath)
64
+ : pathEntryExists(userPath)
46
65
  ? userPath
47
66
  : null;
67
+ }
68
+
69
+ export function loadConfig({ cwd, home, required = [], allowMissing = false }) {
70
+ const selectedPath = selectConfigPath({ cwd, home });
71
+ if (!selectedPath && allowMissing && required.length === 0) {
72
+ return { path: null, values: {} };
73
+ }
48
74
 
49
75
  if (!selectedPath) {
76
+ const projectPath = path.join(cwd, ".pi", "cadence", ".env");
77
+ const userPath = path.join(home, ".pi", "agent", "cadence", ".env");
50
78
  throw new Error(
51
79
  `No cadence configuration file found; create ${projectPath} or ${userPath}`,
52
80
  );
53
81
  }
54
82
 
55
- const values = parseEnvFile(readFileSync(selectedPath, "utf8"));
83
+ // Configuration inspection also runs at extension startup: never block on
84
+ // a FIFO/device or read an unbounded file. Read one extra byte to detect growth.
85
+ const maxBytes = 64 * 1024;
86
+ const fd = openSync(selectedPath, constants.O_RDONLY | constants.O_NONBLOCK);
87
+ let content;
88
+ try {
89
+ const stat = fstatSync(fd);
90
+ if (!stat.isFile() || stat.size > maxBytes) {
91
+ throw new Error(
92
+ "Cadence configuration must be a regular file of at most 64 KiB",
93
+ );
94
+ }
95
+ const buffer = Buffer.alloc(maxBytes + 1);
96
+ let size = 0;
97
+ while (size < buffer.length) {
98
+ const count = readSync(fd, buffer, size, buffer.length - size, null);
99
+ if (!count) break;
100
+ size += count;
101
+ }
102
+ if (size > maxBytes)
103
+ throw new Error("Cadence configuration exceeds 64 KiB");
104
+ content = buffer.subarray(0, size).toString("utf8");
105
+ } finally {
106
+ closeSync(fd);
107
+ }
108
+ const values = parseEnvFile(content);
56
109
  const missing = required.filter((name) => !values[name]);
57
110
  if (missing.length > 0) {
58
111
  throw new Error(
@@ -30,3 +30,8 @@ Treat a non-zero exit, invalid shape, or empty useful result as a failed lookup;
30
30
  The CLI selects the project `.pi/cadence/.env` as a whole file when present, otherwise `~/.pi/agent/cadence/.env`.
31
31
  It does not merge files, read ambient API configuration, or read `.env` beside the installed package.
32
32
  `CONTEXT7_API_KEY` may be empty; `CONTEXT7_API_URL` defaults to `https://context7.com/api/v2`.
33
+ When neither configuration file exists, use those anonymous defaults without creating a file.
34
+ An existing malformed or unreadable file is still an error; never silently fall back.
35
+ Cadence shows a local configuration card on every extension load in TUI/RPC; configured does not mean network-verified.
36
+ Configuration files must be regular files of at most 64 KiB.
37
+ Never ask for API keys in chat.
@@ -9,7 +9,7 @@ import { loadConfig } from "../_shared/load-config.mjs";
9
9
  const DEFAULT_API_URL = "https://context7.com/api/v2";
10
10
 
11
11
  export function loadContext7Config({ cwd, home }) {
12
- const selected = loadConfig({ cwd, home });
12
+ const selected = loadConfig({ cwd, home, allowMissing: true });
13
13
  return {
14
14
  path: selected.path,
15
15
  apiUrl: selected.values.CONTEXT7_API_URL || DEFAULT_API_URL,
@@ -36,4 +36,9 @@ Treat non-zero exits and invalid result shapes as failed research.
36
36
 
37
37
  The CLI selects the project `.pi/cadence/.env` as a whole file when present, otherwise `~/.pi/agent/cadence/.env`.
38
38
  `GROK_API_URL` and `GROK_API_KEY` are required.
39
+ Cadence shows the selected configuration path and missing fields on every extension load in TUI/RPC, without network verification.
40
+ If configuration is missing, direct the user to edit that local file (prefer the user file when no project file exists); never ask them to paste a key in chat.
41
+ The CLI does not read shell API variables.
42
+ Save one `NAME=value` per line; the next invocation reads the changes without restarting Pi.
43
+ Configuration files must be regular files of at most 64 KiB.
39
44
  `GROK_MODEL` defaults to `grok-4.20-non-reasoning`, `TAVILY_API_URL` defaults to `https://api.tavily.com`, and Tavily is enabled by default once a non-empty `TAVILY_API_KEY` is configured; set `TAVILY_ENABLED=false` to opt out.
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export { executePackageVerification } from "./package-verification.ts";
22
22
  // integration; abel-init and ordinary prompts never activate dispatch.
23
23
 
24
24
  import { createHash } from "node:crypto";
25
+ import { homedir } from "node:os";
25
26
  import { dirname, join } from "node:path";
26
27
  import { fileURLToPath } from "node:url";
27
28
  import type {
@@ -58,7 +59,7 @@ import { canonicalJson } from "./implement-graph.ts";
58
59
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
59
60
 
60
61
  import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
61
-
62
+ import { STARTUP_CARD_KEY, startupCardLines } from "./startup-card.ts";
62
63
  import { StateRootError } from "./state-root.ts";
63
64
  import {
64
65
  ACTIVITY_DETAILS_KEY,
@@ -1792,4 +1793,22 @@ export function registerWorkflowControl(
1792
1793
 
1793
1794
  export default function register(pi: ExtensionAPI): void {
1794
1795
  registerWorkflowControl(pi);
1796
+ // Presentation only: never persist a dismissal or inject a model message.
1797
+ pi.on("session_start", (_event, ctx) => {
1798
+ if (!ctx.hasUI) return;
1799
+ ctx.ui.setWidget(
1800
+ STARTUP_CARD_KEY,
1801
+ startupCardLines({
1802
+ cwd: ctx.cwd,
1803
+ home: homedir(),
1804
+ projectTrusted: ctx.isProjectTrusted(),
1805
+ }),
1806
+ );
1807
+ });
1808
+ pi.on("agent_start", (_event, ctx) => {
1809
+ if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
1810
+ });
1811
+ pi.on("session_shutdown", (_event, ctx) => {
1812
+ if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
1813
+ });
1795
1814
  }
@@ -0,0 +1,6 @@
1
+ export declare const CONFIG_TEMPLATE: string;
2
+
3
+ export declare function ensureUserConfig(home?: string): {
4
+ created: boolean;
5
+ path: string;
6
+ };
@@ -0,0 +1,57 @@
1
+ import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const CONFIG_TEMPLATE = `# Cadence research configuration. Keep this file local and never commit it.
7
+ # Context7 works anonymously with its default URL.
8
+ # CONTEXT7_API_URL=https://context7.com/api/v2
9
+ # CONTEXT7_API_KEY=
10
+
11
+ # Required when using the Grok search skill.
12
+ GROK_API_URL=https://api.x.ai/v1
13
+ GROK_API_KEY=
14
+
15
+ # Optional Tavily integration.
16
+ # TAVILY_ENABLED=true
17
+ # TAVILY_API_URL=https://api.tavily.com
18
+ # TAVILY_API_KEY=
19
+ `;
20
+
21
+ export function ensureUserConfig(home = homedir()) {
22
+ const directory = path.join(home, ".pi", "agent", "cadence");
23
+ const file = path.join(directory, ".env");
24
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
25
+ try {
26
+ const fd = openSync(
27
+ file,
28
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
29
+ 0o600,
30
+ );
31
+ try {
32
+ writeSync(fd, CONFIG_TEMPLATE, undefined, "utf8");
33
+ } finally {
34
+ closeSync(fd);
35
+ }
36
+ return { created: true, path: file };
37
+ } catch (error) {
38
+ if (error?.code === "EEXIST") return { created: false, path: file };
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ if (
44
+ process.argv[1] &&
45
+ path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
46
+ ) {
47
+ try {
48
+ const result = ensureUserConfig();
49
+ if (result.created)
50
+ process.stderr.write(`Cadence created ${result.path}\n`);
51
+ } catch (error) {
52
+ process.stderr.write(
53
+ `Cadence could not create the user configuration template: ${error instanceof Error ? error.message : String(error)}\n`,
54
+ );
55
+ process.exitCode = 0;
56
+ }
57
+ }
@@ -0,0 +1,84 @@
1
+ import path from "node:path";
2
+ import {
3
+ loadConfig,
4
+ selectConfigPath,
5
+ } from "../skills/_shared/load-config.mjs";
6
+
7
+ export const STARTUP_CARD_KEY = "cadence-configuration";
8
+
9
+ // Only code-owned labels and paths enter the UI; never values, endpoints,
10
+ // models, parser errors or credentials from the configuration file.
11
+ function displayPath(value: string): string {
12
+ return JSON.stringify(value).slice(1, -1);
13
+ }
14
+
15
+ function validUrl(value: string): boolean {
16
+ try {
17
+ const url = new URL(value);
18
+ return url.protocol === "https:" || url.protocol === "http:";
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ export function startupCardLines(options: {
25
+ cwd: string;
26
+ home: string;
27
+ projectTrusted: boolean;
28
+ }): string[] {
29
+ const lines = ["Cadence · 研究能力(仅本地配置检查,未联网验证)"];
30
+ if (!options.projectTrusted) {
31
+ return [
32
+ ...lines,
33
+ "尚未信任当前项目:未读取研究配置;信任项目后重新加载可查看状态。",
34
+ "不自动启动 Abel 工作流;请勿在聊天中发送 API Key。",
35
+ ];
36
+ }
37
+
38
+ let selectedPath: string | null = null;
39
+ try {
40
+ selectedPath = selectConfigPath(options);
41
+ const { values } = loadConfig({ ...options, allowMissing: true });
42
+ const context7 = !validUrl(
43
+ values.CONTEXT7_API_URL || "https://context7.com/api/v2",
44
+ )
45
+ ? "CONTEXT7_API_URL 格式无效"
46
+ : values.CONTEXT7_API_KEY
47
+ ? "已配置密钥"
48
+ : "匿名模式,无需密钥";
49
+ const missing = ["GROK_API_URL", "GROK_API_KEY"].filter(
50
+ (name) => !values[name],
51
+ );
52
+ const grok = missing.length
53
+ ? `待配置 ${missing.join("、")}`
54
+ : !validUrl(values.GROK_API_URL)
55
+ ? "GROK_API_URL 格式无效"
56
+ : "已配置";
57
+ const tavily =
58
+ values.TAVILY_ENABLED === "false"
59
+ ? "已关闭"
60
+ : !values.TAVILY_API_KEY
61
+ ? "未配置(可选)"
62
+ : !validUrl(values.TAVILY_API_URL || "https://api.tavily.com")
63
+ ? "TAVILY_API_URL 格式无效"
64
+ : "已配置(Grok Skill 仍需 Grok 配置)";
65
+ lines.push(
66
+ `文档查询 Context7:${context7}`,
67
+ `联网搜索 Grok:${grok};Tavily:${tavily}`,
68
+ );
69
+ } catch {
70
+ lines.push("研究配置无法读取或解析:请检查下方文件;未回退到其他配置。");
71
+ }
72
+
73
+ const userPath = path.join(options.home, ".pi", "agent", "cadence", ".env");
74
+ const projectPath = path.join(options.cwd, ".pi", "cadence", ".env");
75
+ lines.push(
76
+ selectedPath
77
+ ? `生效文件:${displayPath(selectedPath)}${selectedPath === projectPath ? "(项目整文件优先,不合并用户配置)" : "(用户级,跨项目复用)"}`
78
+ : `配置位置:${displayPath(userPath)}(用户级,跨项目复用)`,
79
+ "在本地编辑该文件:GROK_API_URL=<服务地址>、GROK_API_KEY=<密钥>,每项一行;可选 TAVILY_API_KEY。",
80
+ "研究 Skills 不读取 shell API 变量;保存后下次调用生效。不要把密钥发到聊天或提交到 Git。",
81
+ "可选服务未配置不阻塞普通任务,也不自动启动 Abel 工作流。",
82
+ );
83
+ return lines;
84
+ }