@enter-pro/enter-cli 0.4.2 → 0.4.4

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.
@@ -6,7 +6,7 @@ export const whoamiCmd = new Command("whoami")
6
6
  .description("Show current user info")
7
7
  .action(async (_opts, cmd) => {
8
8
  if (!isAuthenticated()) {
9
- throw new Error("Not authenticated. Run `enter login` or set ENTER_API_KEY environment variable.");
9
+ throw new Error("Not authenticated. Run `enter-cli login` or set ENTER_API_KEY environment variable.");
10
10
  }
11
11
  const data = await client.get("/v1/users/info");
12
12
  const format = cmd.optsWithGlobals().output || "json";
@@ -1,12 +1,9 @@
1
1
  import { Command } from "commander";
2
2
  import * as client from "../client.js";
3
- import { print, printMessage, printTable, pickList } from "../output.js";
3
+ import { print, printResult, printTable, pickList, getFormat } from "../output.js";
4
4
  export const workspaceCmd = new Command("workspace")
5
5
  .alias("ws")
6
6
  .description("Manage workspaces");
7
- function getFormat(cmd) {
8
- return cmd.optsWithGlobals().output || "json";
9
- }
10
7
  workspaceCmd
11
8
  .command("list")
12
9
  .description("List workspaces")
@@ -68,9 +65,9 @@ workspaceCmd
68
65
  workspaceCmd
69
66
  .command("delete <workspace_id>")
70
67
  .description("Delete a workspace")
71
- .action(async (id) => {
68
+ .action(async (id, _opts, cmd) => {
72
69
  await client.del(`/v1/workspaces/${id}`);
73
- printMessage("Workspace deleted successfully.");
70
+ printResult(getFormat(cmd), { deleted: true, workspace_id: id }, "Workspace deleted successfully.");
74
71
  });
75
72
  // Members subcommand group
76
73
  const membersCmd = new Command("members").description("Manage workspace members");
@@ -118,7 +115,7 @@ membersCmd
118
115
  .description("Remove a member from workspace")
119
116
  .option("--email <email>", "Member email")
120
117
  .option("--user-id <id>", "Member user ID")
121
- .action(async (id, opts) => {
118
+ .action(async (id, opts, cmd) => {
122
119
  if (!opts.email && !opts.userId) {
123
120
  throw new Error("--email or --user-id is required");
124
121
  }
@@ -128,7 +125,7 @@ membersCmd
128
125
  if (opts.userId)
129
126
  body.user_id = Number(opts.userId);
130
127
  await client.post(`/v1/workspaces/${id}/members/remove`, body);
131
- printMessage("Member removed successfully.");
128
+ printResult(getFormat(cmd), { removed: true, workspace_id: id }, "Member removed successfully.");
132
129
  });
133
130
  membersCmd
134
131
  .command("update-role <workspace_id>")
@@ -155,9 +152,9 @@ membersCmd
155
152
  membersCmd
156
153
  .command("leave <workspace_id>")
157
154
  .description("Leave a workspace")
158
- .action(async (id) => {
155
+ .action(async (id, _opts, cmd) => {
159
156
  await client.post(`/v1/workspaces/${id}/leave`);
160
- printMessage("Left workspace successfully.");
157
+ printResult(getFormat(cmd), { left: true, workspace_id: id }, "Left workspace successfully.");
161
158
  });
162
159
  workspaceCmd.addCommand(membersCmd);
163
160
  // Credits subcommand group
package/dist/config.d.ts CHANGED
@@ -2,7 +2,6 @@ export interface Config {
2
2
  api_url: string;
3
3
  base_path: string;
4
4
  output: string;
5
- default_workspace: string;
6
5
  }
7
6
  export declare function configDir(): string;
8
7
  export declare function loadConfig(): Config;
package/dist/config.js CHANGED
@@ -8,7 +8,6 @@ const defaults = {
8
8
  api_url: "https://api.enter.pro",
9
9
  base_path: "/code/api",
10
10
  output: "json",
11
- default_workspace: "",
12
11
  };
13
12
  function ensureConfigDir() {
14
13
  if (!existsSync(CONFIG_DIR)) {
@@ -32,8 +31,6 @@ function getEnvOverrides() {
32
31
  overrides.base_path = process.env.ENTER_BASE_PATH;
33
32
  if (process.env.ENTER_OUTPUT)
34
33
  overrides.output = process.env.ENTER_OUTPUT;
35
- if (process.env.ENTER_DEFAULT_WORKSPACE)
36
- overrides.default_workspace = process.env.ENTER_DEFAULT_WORKSPACE;
37
34
  return overrides;
38
35
  }
39
36
  export function configDir() {
@@ -42,15 +39,24 @@ export function configDir() {
42
39
  export function loadConfig() {
43
40
  const fileConfig = loadFromFile();
44
41
  const envOverrides = getEnvOverrides();
45
- return { ...defaults, ...fileConfig, ...envOverrides };
42
+ const merged = { ...defaults, ...fileConfig, ...envOverrides };
43
+ return Object.fromEntries(Object.keys(defaults).map(key => [key, merged[key]]));
46
44
  }
47
45
  export function setConfig(key, value) {
46
+ validateKey(key);
47
+ if (key === "output" && !["json", "yaml", "table"].includes(value))
48
+ throw new Error("output must be json, yaml or table");
48
49
  ensureConfigDir();
49
50
  const current = loadFromFile();
50
51
  current[key] = value;
51
52
  writeFileSync(CONFIG_FILE, yaml.dump(current), "utf-8");
52
53
  }
54
+ function validateKey(key) {
55
+ if (!Object.hasOwn(defaults, key))
56
+ throw new Error(`Unsupported setting "${key}". Supported settings: ${Object.keys(defaults).join(", ")}. Pass workspace IDs explicitly to project commands.`);
57
+ }
53
58
  export function getConfig(key) {
59
+ validateKey(key);
54
60
  const cfg = loadConfig();
55
61
  return cfg[key] || "";
56
62
  }
package/dist/output.d.ts CHANGED
@@ -6,21 +6,5 @@ export declare function printMessage(msg: string): void;
6
6
  export declare function printResult(format: string, structured: unknown, message: string): void;
7
7
  import type { Command } from "commander";
8
8
  export declare function getFormat(cmd: Command): string;
9
- export declare function printError(err: Error | string): void;
10
9
  export declare function pick<T extends Record<string, unknown>>(obj: T, keys: string[]): Record<string, unknown>;
11
10
  export declare function pickList(items: Record<string, unknown>[], keys: string[]): Record<string, unknown>[];
12
- export interface ListEnvelope {
13
- items: unknown[];
14
- total?: number;
15
- page?: number;
16
- page_size?: number;
17
- }
18
- export interface TableConfig {
19
- headers: string[];
20
- rowMapper: (item: Record<string, unknown>) => string[];
21
- }
22
- export declare function printList(format: string, data: ListEnvelope, tableConfig?: TableConfig): void;
23
- export declare function printSingle(format: string, data: unknown, tableConfig?: {
24
- headers: string[];
25
- rowMapper: (item: Record<string, unknown>) => string[];
26
- }): void;
package/dist/output.js CHANGED
@@ -59,9 +59,6 @@ export function printResult(format, structured, message) {
59
59
  export function getFormat(cmd) {
60
60
  return cmd.optsWithGlobals().output || "json";
61
61
  }
62
- export function printError(err) {
63
- console.error(`Error: ${typeof err === "string" ? err : err.message}`);
64
- }
65
62
  export function pick(obj, keys) {
66
63
  const result = {};
67
64
  for (const key of keys) {
@@ -72,18 +69,3 @@ export function pick(obj, keys) {
72
69
  export function pickList(items, keys) {
73
70
  return items.map((item) => pick(item, keys));
74
71
  }
75
- export function printList(format, data, tableConfig) {
76
- if (format === "table" && tableConfig) {
77
- const rows = data.items.map(tableConfig.rowMapper);
78
- printTable(tableConfig.headers, rows);
79
- return;
80
- }
81
- print(format, data);
82
- }
83
- export function printSingle(format, data, tableConfig) {
84
- if (format === "table" && tableConfig) {
85
- printTable(tableConfig.headers, [tableConfig.rowMapper(data)]);
86
- return;
87
- }
88
- print(format, data);
89
- }
package/dist/poll.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare class TimeoutError extends Error {
2
2
  constructor(elapsedMs: number);
3
3
  }
4
4
  export interface PollOptions {
5
+ signal?: AbortSignal;
5
6
  intervalMs?: number;
6
7
  timeoutMs?: number;
7
8
  onTick?: (elapsed: number, data: unknown) => void;
package/dist/poll.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
1
2
  export class TimeoutError extends Error {
2
3
  constructor(elapsedMs) {
3
4
  super(`Timed out after ${Math.round(elapsedMs / 1000)}s`);
@@ -10,6 +11,7 @@ export async function pollUntil(fetcher, predicate, options) {
10
11
  const onTick = options?.onTick;
11
12
  const start = Date.now();
12
13
  while (true) {
14
+ options?.signal?.throwIfAborted();
13
15
  const data = await fetcher();
14
16
  const elapsed = Date.now() - start;
15
17
  if (predicate(data)) {
@@ -19,6 +21,6 @@ export async function pollUntil(fetcher, predicate, options) {
19
21
  throw new TimeoutError(elapsed);
20
22
  }
21
23
  onTick?.(elapsed, data);
22
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
24
+ await delay(Math.min(intervalMs, Math.max(0, timeoutMs - elapsed)), undefined, { signal: options?.signal });
23
25
  }
24
26
  }
@@ -30,6 +30,7 @@ export declare class ThreadEvents {
30
30
  private wake;
31
31
  private setMode;
32
32
  private connect;
33
+ private connectAuthenticated;
33
34
  wait(afterRevision: number, timeoutMs: number): Promise<void>;
34
35
  close(): void;
35
36
  }
@@ -2,7 +2,7 @@ import WebSocket from "ws";
2
2
  import { HttpsProxyAgent } from "https-proxy-agent";
3
3
  import { getProxyForUrl } from "proxy-from-env";
4
4
  import { baseURL } from "./config.js";
5
- import { getToken } from "./auth.js";
5
+ import { getValidToken } from "./auth.js";
6
6
  import { RequestError } from "./errors.js";
7
7
  export function streamURL(base, projectId, options) {
8
8
  const url = new URL(`${base.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(projectId)}/thread/stream`);
@@ -77,10 +77,20 @@ export class ThreadEvents {
77
77
  this.wake();
78
78
  }
79
79
  connect() {
80
+ void this.connectAuthenticated().catch(error => {
81
+ if (this.stopped)
82
+ return;
83
+ this.failure = new RequestError("STREAM_SETUP_ERROR", error instanceof Error ? error.message : "Could not initialize Enter event stream.");
84
+ this.setMode("polling", "stream_setup_failed");
85
+ });
86
+ }
87
+ async connectAuthenticated() {
80
88
  if (this.stopped)
81
89
  return;
82
90
  const url = streamURL(baseURL(), this.projectId, { ...this.options, cursor: this.cursor });
83
- const token = getToken();
91
+ const token = await getValidToken(this.options.signal);
92
+ if (this.stopped)
93
+ return;
84
94
  // proxy-from-env honors NO_PROXY, including loopback fixture servers.
85
95
  const proxy = process.env.NODE_USE_ENV_PROXY === "0" ? "" : getProxyForUrl(url.toString().replace(/^ws/, "http"));
86
96
  const socket = new WebSocket(url, {
@@ -100,7 +110,10 @@ export class ThreadEvents {
100
110
  if ([401, 403].includes(status)) {
101
111
  retryable = false;
102
112
  reason = status === 401 ? "authentication_required" : "permission_denied";
103
- this.failure = new RequestError("STREAM_AUTH_ERROR", "Enter event stream rejected access. Check authentication and project permissions.");
113
+ // State observers retain authoritative HTTP polling; raw message followers cannot.
114
+ if (!this.options.stateOnly) {
115
+ this.failure = new RequestError("STREAM_AUTH_ERROR", "Enter event stream rejected access. Check authentication and project permissions.");
116
+ }
104
117
  }
105
118
  else if ([404, 405, 426, 501].includes(status)) {
106
119
  retryable = false;
@@ -0,0 +1,46 @@
1
+ export type ThreadStatus = 'idle' | 'running' | 'queued' | 'pending' | 'blocked' | 'completed' | 'failed' | 'unknown';
2
+ export type InputKind = 'none' | 'secret' | 'questions' | 'auth_provider';
3
+ export interface WorkflowSnapshot {
4
+ status: ThreadStatus;
5
+ task_id?: unknown;
6
+ turn?: Record<string, unknown> | null;
7
+ actions?: Record<string, unknown>[];
8
+ project?: Record<string, unknown>;
9
+ reason?: string;
10
+ interrupted?: boolean;
11
+ wait_timed_out?: boolean;
12
+ observation_retrying?: boolean;
13
+ progress_changed?: boolean;
14
+ error?: {
15
+ retryable?: boolean;
16
+ };
17
+ }
18
+ /** Host-facing workflow decisions, independent of transport and backend phases. */
19
+ export declare function workflowResult(snapshot: WorkflowSnapshot): {
20
+ actions?: {
21
+ id: unknown;
22
+ kind: unknown;
23
+ decision: string;
24
+ required_fields: {};
25
+ submit_command: unknown;
26
+ }[] | undefined;
27
+ build?: {
28
+ commit: {} | null;
29
+ matches_task: boolean;
30
+ success: {} | null;
31
+ } | undefined;
32
+ observation: {
33
+ timed_out: boolean;
34
+ retrying: boolean;
35
+ interrupted: boolean;
36
+ };
37
+ reason?: string | undefined;
38
+ after_reporting?: string | undefined;
39
+ state: string;
40
+ task: {
41
+ id: {} | null;
42
+ turn: {} | null;
43
+ chat_id: {} | null;
44
+ };
45
+ next_action: string;
46
+ };
@@ -0,0 +1,36 @@
1
+ /** Host-facing workflow decisions, independent of transport and backend phases. */
2
+ export function workflowResult(snapshot) {
3
+ const turn = snapshot.turn;
4
+ const actions = snapshot.actions ?? [];
5
+ const build = snapshot.project?.build_status;
6
+ const state = snapshot.status === 'blocked' ? 'needs_input'
7
+ : snapshot.status === 'queued' || snapshot.status === 'pending' ? 'running' : snapshot.status;
8
+ const next = snapshot.interrupted ? 'resume_when_requested'
9
+ : snapshot.error && !snapshot.error.retryable ? 'resolve_error'
10
+ : state === 'needs_input' ? 'handle_actions'
11
+ : state === 'completed' ? 'read_delivery'
12
+ : state === 'failed' ? 'report_failure'
13
+ : snapshot.progress_changed ? 'report_progress'
14
+ : state === 'idle' ? 'none'
15
+ : 'observe';
16
+ return {
17
+ state,
18
+ task: { id: snapshot.task_id ?? turn?.task_id ?? turn?.id ?? null, turn: turn?.turn ?? null, chat_id: turn?.chat_id ?? null },
19
+ next_action: next,
20
+ ...(next === "report_progress" ? { after_reporting: "observe" } : {}),
21
+ ...(snapshot.reason ? { reason: snapshot.reason } : {}),
22
+ observation: { timed_out: Boolean(snapshot.wait_timed_out), retrying: Boolean(snapshot.observation_retrying), interrupted: Boolean(snapshot.interrupted) },
23
+ ...(snapshot.project ? { build: {
24
+ commit: build?.commit_id ?? null,
25
+ matches_task: Boolean(turn?.commit_id && build?.commit_id === turn.commit_id),
26
+ success: build?.success ?? null,
27
+ } } : {}),
28
+ ...(actions.length ? { actions: actions.map(action => ({
29
+ id: action.action_id,
30
+ kind: action.tool_name === 'confirm_plan_mode' ? 'plan' : action.input_kind === 'none' ? 'confirmation' : action.input_kind,
31
+ decision: action.tool_name === 'confirm_plan_mode' || action.input_kind === 'questions' ? 'ask_user' : 'use_existing_authorization',
32
+ required_fields: action.configuration?.fields ?? (action.input_kind === 'secret' ? (action.tool_name === 'supabase_add_secret' ? ['secret_name', 'secret_value'] : ['secret_value']) : []),
33
+ submit_command: action.approve_command,
34
+ })) } : {}),
35
+ };
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enter-pro/enter-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Enter CLI - manage Enter platform resources from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,10 +14,14 @@
14
14
  "test": "npm run build && node --test",
15
15
  "mock:serve": "node scripts/mock-enter.mjs",
16
16
  "local:cli": "node scripts/local-cli.mjs",
17
- "prepublishOnly": "npm run build"
17
+ "prepublishOnly": "npm run build",
18
+ "install:hosts": "node scripts/install-hosts.mjs"
18
19
  },
19
20
  "files": [
20
- "dist"
21
+ "dist",
22
+ "skills",
23
+ "scripts/install-hosts.mjs",
24
+ "README.md"
21
25
  ],
22
26
  "publishConfig": {
23
27
  "access": "public"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { readFileSync, writeFileSync, mkdirSync, cpSync, chmodSync, realpathSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { resolve, join } from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ const { values } = parseArgs({ options: { package: { type: 'string' }, host: { type: 'string', default: 'all' }, home: { type: 'string', default: homedir() } } });
9
+ if (!values.package || !['all', 'codex', 'dsh'].includes(values.host)) throw Error('Usage: install-hosts.mjs --package /path/package.tgz [--host all|codex|dsh] [--home /path]');
10
+ const archive = realpathSync(values.package);
11
+ const hash = createHash('sha256').update(readFileSync(archive)).digest('hex');
12
+ const quote = s => "'" + s.replaceAll("'", "'\\''") + "'";
13
+ for (const host of values.host === 'all' ? ['codex', 'dsh'] : [values.host]) {
14
+ const root = join(resolve(values.home), '.' + host);
15
+ const prefix = join(root, 'tools/enter-cli');
16
+ const result = spawnSync('npm', ['install', '--prefix', prefix, '--ignore-scripts', '--no-audit', '--no-fund', archive], { stdio: 'inherit' });
17
+ if (result.status !== 0) throw Error(`Installation failed for ${host}`);
18
+ const installed = join(prefix, 'node_modules/@enter-pro/enter-cli');
19
+ const skill = join(root, 'skills/enter');
20
+ mkdirSync(join(skill, 'scripts'), { recursive: true });
21
+ cpSync(join(installed, 'skills/enter/references'), join(skill, 'references'), { recursive: true });
22
+ const wrapper = join(skill, 'scripts/enter-cli');
23
+ writeFileSync(wrapper, '#!/bin/sh\nset -eu\nexport NODE_USE_ENV_PROXY="${NODE_USE_ENV_PROXY:-1}"\nexec ' + quote(realpathSync(process.execPath)) + ' ' + quote(join(installed, 'dist/index.js')) + ' "$@"\n');
24
+ chmodSync(wrapper, 0o755);
25
+ writeFileSync(join(skill, 'SKILL.md'), readFileSync(join(installed, 'skills/enter/SKILL.md'), 'utf8').replaceAll('{{CLI_ENTRY}}', wrapper));
26
+ const pkg = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8'));
27
+ writeFileSync(join(prefix, 'install-info.json'), JSON.stringify({ version: pkg.version, package_sha256: hash, installed_at: new Date().toISOString(), source_archive: archive }, null, 2) + '\n');
28
+ console.log(`${host}: installed ${pkg.version}, sha256 ${hash}`);
29
+ }
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: enter
3
+ description: 使用 Enter CLI 创建和修改网站或应用,跟踪任务、处理提问与配置卡,并交付 Enter 的构建和验证结果。
4
+ ---
5
+
6
+ # Enter
7
+
8
+ 用户描述需求,Enter 负责开发与视觉/功能验证,宿主通过 CLI 提交、收集输入和交付。
9
+
10
+ ## 入口
11
+
12
+ 使用 `{{CLI_ENTRY}}`。第一次调用核对 `--version` 和登录状态;命令参数按需查 `--help`。输出优先用 JSON,保持 stdout/stderr 分离;不要回显完整身份资料或凭据。
13
+
14
+ ## 流程
15
+
16
+ 1. 从用户上下文确定项目;新建时确定 workspace。通过 `project create` 或 `thread chat` 提交一次,保存 task_id。accepted 或观察超时不代表完成,也不是重新提交的理由。
17
+ 2. 按下方宿主能力规则选择返回的 stream_command 或 wait_command,按同一 task_id 继续;需交付构建时加 `--require-build`。新增需求单独记录返回的 task_id。
18
+ 3. 状态的 `workflow` 给出任务关联、next_action 和构建匹配信息。遇到 `handle_actions` 按下方卡片规则处理;`read_delivery` 执行 messages_command,取得 Enter 的总结与验证证据。未知关联不代表提交失败,不猜测它属于其他已完成任务。
19
+ 4. 依用户已有授权继续,不因阶段切换反复确认。明确要求用户决策的计划和提问仍需转交。写操作超时或解析失败先查真实状态,再决定是否重试。
20
+ 5. 交付说明完成内容、预览/发布地址、Enter 验证结果及待配置项;代码存在、配置保存、构建成功、真实服务可用分开判断。发布按用户授权范围执行。
21
+
22
+ ## 观察与响应
23
+
24
+ - CLI 用 HTTP 获取真实状态与 Enter 最新进度原文。宿主作为传话者:`workflow.next_action=report_progress` 时先转述 `progress.messages`,再按 `after_reporting=observe` 续接;处理其他 `workflow.next_action`,执行返回的命令;不翻原始日志自行分析开发阶段,不把 Enter 的自述当独立验收。`progress.available=false` 表示暂时无法获取进度,不能说没有进展;有数据但 revision 未变才是没有新内容。不编造百分比,不用额度消耗、时间戳变化证明进展。默认只读取监控字段,除非用户查询详情,不使用 --full。
25
+ - 首选单个 `stream_command`(`thread wait --stream --timeout 0`):仅在宿主支持后台执行、增量输出通知和同时处理用户消息时使用。CLI 内部轮询,宿主非阻塞消费阶段性事件;运行期间不要另起 wait,不因 report_progress 重启观察。收到最终 result 后再按状态处理;不使用 shell & 脱离宿主管理。watch 保留为同一流式观察能力的兼容入口。
26
+ - 同步或仅有完成通知的宿主回退到 `wait_command`:它在普通进度合并窗口到达且有新进展时退出,所以只支持任务完成通知的后台宿主,也能被新进展唤醒。支持后台任务时在后台运行并用完成通知续接,不阻塞等整个网站完成;同步宿主直接执行有界 wait。
27
+ - `wait` 返回 `progress_changed=true` 时直接简短转述,再执行带 `--after-progress` 的续接命令。只有确认宿主能在新 stdout 到达时唤醒 Agent,才选择 `watch_command`;仅能手动读取运行中输出还不够。不得用反复 job_output 空读替代通知,也不得阻塞读取直到后台任务结束。watch 无限观察仅显式 `--timeout 0`。
28
+ - 按 CLI 合并后的进展简短汇报;细碎技术变化无需逐条复述。面向用户不解释 CLI 协议、job 状态或调度过程。相同 revision、heartbeat 和无变化超时无需重复刷屏。超时结束的是本次观察,不是远端工作。保持同一 task_id、turn、chat 和构建要求,不重提、不询问是否继续,不写 sleep 或 status 循环。
29
+ - 用户插话时立即用最近已知的进度与 `observed_at` 回答,标明是上次观察;之后按需刷新一次 status,再继续原任务。不要为了拿最新状态先阻塞回答。新增需求才提交 chat,单纯问进度不触发构建。
30
+ - 普通进度默认每 10 秒合并一次(`--progress-interval`),保留最新原文,不猜测阶段;问题卡、完成和失败立即返回。保留返回的间隔与时限,不自行调长等待。
31
+ - `wait` 默认最多 10 秒,`watch` 默认 60 秒,显式时限会保留在续接命令中。这是观察上限,不是心跳周期或响应保证;不得为等到完成不断延长阻塞调用。宿主如果本身不支持插话或后台唤醒,CLI 不能保证即时响应或主动通知。
32
+
33
+ ## 卡片
34
+
35
+ - 计划:展示 plan,等待用户同意或修改意见,展示不等于批准。
36
+ - ask_user_question:转交所有问题、选项和单/多选要求,收集答案后用 --answers 回传。JSON 的键是 Enter 原始问题全文,不是宿主提问工具生成的 ID;值是 {"selected_options":["选项原文"],"other_text":"自由文字"},纯文字回答使用空数组。只有用户要求跳过时才执行 skip_command。
37
+ - 普通确认:已有授权就 approve;需要新的决定时询问。用户拒绝才 reject。
38
+ - 配置输入:已有授权和值/文件就提交;缺什么只问什么。普通配置可直接询问,真实密钥使用可用的安全输入或用户指定文件;表单是替代入口,不强制绕路。OAuth/secret/Stripe 的字段、stdin 和更新方法见 [配置输入](references/configuration.md),仅遇到这些卡片时读取。
39
+
40
+ CLI 负责 HTTP 观察和可选的事件监听;宿主负责后台调度与用户交互。等待工具不自动批准卡片。宿主没有浏览器不影响 Enter 内部验证,不以关键词扫描替代其验证结果。
@@ -0,0 +1,19 @@
1
+ # 配置输入
2
+
3
+ 遇到 OAuth、Stripe 或 secret 卡时读取。目标是让用户提供一次所缺信息,然后由 CLI 保存、批准并继续任务。
4
+
5
+ 1. 读取卡片字段和现有任务上下文。已有获授权的值或文件直接使用;缺少字段时只说明字段用途及可用输入方式,不默认要求切换网页,也不反复确认已有授权。
6
+ 2. 非敏感配置(例如 GA Measurement ID)可直接询问。真实密钥使用宿主已有安全输入或用户指定文件;没有这些渠道时给出 Enter 项目表单入口。不要虚构宿主弹窗。用户主动提供的值可按当前授权处理,避免再次复制;若工具无法在不暴露值的情况下提交,说明具体限制并改用文件或表单。
7
+ 3. OAuth JSON 字段以卡片 configuration.fields 为准。Google 示例结构为 client_ids、client_secret;只在确实需要时给模板。文件通过重定向送入 stdin,命令中只有路径:
8
+
9
+ ```sh
10
+ <CLI> --output json thread approve <PROJECT> <ACTION> --auth-config-stdin < /absolute/path/oauth.json
11
+ <CLI> --output json thread approve <PROJECT> <ACTION> --secret-value-stdin < /absolute/path/stripe-key.txt
12
+ <CLI> --output json thread approve <PROJECT> <ACTION> --secret-name <NAME> --secret-value-stdin < /absolute/path/secret.txt
13
+ ```
14
+
15
+ `<CLI>` 使用技能入口的实际路径。不要 cat 文件、把值插入 printf/heredoc 或复制进 Enter 构建提示;这些都会进入工具记录。stdin 只避免 CLI 参数暴露,不能消除上游聊天或工具记录。不要在命令中合并 stderr 与需要 JSON 解析的 stdout。
16
+
17
+ 4. 表单保存可能已批准卡片,先查状态;CLI 批准后按返回 task_id 继续。不重复要求用户填写。OAuth 回调地址以实际配置返回或表单为准;需要服务商配置时提供确切地址,不猜测,也不声称该地址只能从某个入口取得。
18
+ 5. 提交失败先查状态再决定是否重试;例如 failed to connect Stripe 只说明连接失败,未证明具体原因。缺输入时等待补填,不自行拒绝卡片。用户明确暂不接入才 reject。
19
+ 6. 读取 Enter 的交付总结和验证证据。分别说明配置已保存、功能已接线、构建/发布状态、真实服务是否验证;mock 值可以验证流程,不能证明登录或支付成功。需要换值时沿同一配置流程更新,保持用户已选择的发布范围。