@hmj-ai/cflow 1.1.0 → 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.
@@ -6,8 +6,8 @@
6
6
  <meta name="color-scheme" content="light" />
7
7
  <link rel="icon" href="data:," />
8
8
  <title>CF Platform</title>
9
- <script type="module" crossorigin src="/assets/index-BqTfYp5s.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-D0BpmA_V.css">
9
+ <script type="module" crossorigin src="/assets/index-DsBDM4p-.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-C1_cd_s8.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
package/dist/src/db.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { mkdirSync } from 'node:fs';
3
- import { dirname } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  export class Store {
5
5
  db;
6
- constructor(file = process.env.CF_DB ?? './data/cf.sqlite') {
6
+ constructor(file = join(process.cwd(), '.cflow', 'cflow.sqlite')) {
7
7
  mkdirSync(dirname(file), { recursive: true });
8
8
  this.db = new Database(file);
9
9
  this.db.pragma('journal_mode = WAL');
@@ -2,29 +2,96 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, isAbsolute, join, resolve } from 'node:path';
5
- const builtinManifests = [
5
+ export const builtinAdapterDescriptors = [
6
6
  {
7
- schemaVersion: 1,
8
- id: 'codex',
9
- name: 'Codex',
10
- description: '用本机已登录的 Codex 来执行步骤。默认只读;能力声明 workspace 写入后可修改工作区文件。',
11
- backend: 'acp',
12
- command: 'codex-acp',
13
- envAllowlist: ['HOME', 'PATH', 'LANG', 'LC_ALL', 'CODEX_HOME', 'OPENAI_API_KEY'],
14
- capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
15
- traits: { tokenAccounting: 'approximate' },
7
+ manifest: {
8
+ schemaVersion: 1,
9
+ id: 'codex',
10
+ name: 'Codex',
11
+ description: '用 CFlow 随包提供的 Codex 来执行步骤。默认只读;能力声明 workspace 写入后可修改工作区文件。',
12
+ backend: 'acp',
13
+ command: 'codex-acp',
14
+ envAllowlist: ['HOME', 'LANG', 'LC_ALL', 'CODEX_HOME', 'OPENAI_API_KEY'],
15
+ capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
16
+ traits: { tokenAccounting: 'approximate' },
17
+ },
18
+ commandAliases: ['codex-acp'],
19
+ bundled: true,
20
+ modelEnvironment: 'CODEX_CONFIG',
21
+ nativeCommand: {
22
+ adapterEnvironment: 'CODEX_PATH',
23
+ overrideEnvironment: 'CFLOW_CODEX_PATH',
24
+ bundled: 'codex',
25
+ },
26
+ permissionMode: {
27
+ environment: 'INITIAL_AGENT_MODE',
28
+ readOnly: 'read-only',
29
+ workspaceWrite: 'workspace-write',
30
+ },
31
+ staticEnvironment: { NO_BROWSER: '1' },
16
32
  },
17
33
  {
18
- schemaVersion: 1,
19
- id: 'claude-code',
20
- name: 'Claude Code',
21
- description: '用本机已登录的 Claude Code 来执行步骤。',
22
- backend: 'acp',
23
- command: 'claude-agent-acp',
24
- envAllowlist: ['HOME', 'PATH', 'LANG', 'LC_ALL', 'ANTHROPIC_API_KEY'],
25
- capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
34
+ manifest: {
35
+ schemaVersion: 1,
36
+ id: 'claude-code',
37
+ name: 'Claude Code',
38
+ description: '用 CFlow 随包提供的 Claude Code 来执行步骤。',
39
+ backend: 'acp',
40
+ command: 'claude-agent-acp',
41
+ envAllowlist: ['HOME', 'LANG', 'LC_ALL', 'ANTHROPIC_API_KEY'],
42
+ capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
43
+ },
44
+ commandAliases: ['claude-agent-acp'],
45
+ bundled: true,
46
+ modelEnvironment: 'CLAUDE_MODEL_CONFIG',
47
+ nativeCommand: {
48
+ adapterEnvironment: 'CLAUDE_CODE_EXECUTABLE',
49
+ overrideEnvironment: 'CFLOW_CLAUDE_PATH',
50
+ bundled: 'adapter',
51
+ },
52
+ },
53
+ ];
54
+ export const externalAdapterDescriptors = [
55
+ {
56
+ manifest: {
57
+ schemaVersion: 1,
58
+ id: 'amp',
59
+ name: 'Amp',
60
+ description: '通过本机安装的 Amp ACP adapter 执行步骤。',
61
+ backend: 'acp',
62
+ command: 'amp-acp',
63
+ },
64
+ commandAliases: ['amp-acp'],
65
+ bundled: false,
66
+ },
67
+ {
68
+ manifest: {
69
+ schemaVersion: 1,
70
+ id: 'pi',
71
+ name: 'Pi',
72
+ description: '通过本机安装的 Pi ACP adapter 执行步骤。',
73
+ backend: 'acp',
74
+ command: 'pi-acp',
75
+ },
76
+ commandAliases: ['pi-acp'],
77
+ bundled: false,
78
+ },
79
+ {
80
+ manifest: {
81
+ schemaVersion: 1,
82
+ id: 'github-copilot',
83
+ name: 'GitHub Copilot',
84
+ description: '通过本机安装的 GitHub Copilot ACP 模式执行步骤。',
85
+ backend: 'acp',
86
+ command: 'copilot',
87
+ args: ['--acp'],
88
+ },
89
+ commandAliases: ['copilot'],
90
+ bundled: false,
26
91
  },
27
92
  ];
93
+ const adapterDescriptors = [...builtinAdapterDescriptors, ...externalAdapterDescriptors];
94
+ export const adapterDescriptorForId = (id) => adapterDescriptors.find((descriptor) => descriptor.manifest.id === id);
28
95
  const manifestHash = (manifest) => createHash('sha256').update(JSON.stringify(manifest)).digest('hex').slice(0, 16);
29
96
  const stringValue = (value, name, max) => {
30
97
  if (typeof value !== 'string' || !value.trim() || value.length > max)
@@ -156,6 +223,7 @@ const record = (value, source, manifestPath, baseDirectory) => {
156
223
  const manifest = normalizeManifest(value, baseDirectory);
157
224
  return { manifest, source, manifestPath, manifestHash: manifestHash(manifest) };
158
225
  };
226
+ export const adapterDescriptorRecord = (descriptor, source) => record(descriptor.manifest, source, `descriptor:${descriptor.manifest.id}`);
159
227
  const manifestFiles = (directory) => {
160
228
  if (!directory || !existsSync(directory))
161
229
  return [];
@@ -201,8 +269,8 @@ export function loadAgentManifestRecords(options = {}) {
201
269
  const records = [];
202
270
  const warnings = [];
203
271
  if (options.includeBuiltins !== false)
204
- for (const manifest of builtinManifests)
205
- records.push(record(manifest, 'builtin', `builtin:${manifest.id}`));
272
+ for (const descriptor of builtinAdapterDescriptors)
273
+ records.push(adapterDescriptorRecord(descriptor, 'builtin'));
206
274
  const addValues = (values, source, manifestPath, baseDirectory) => {
207
275
  const manifests = Array.isArray(values) ? values : [values];
208
276
  for (const value of manifests) {
@@ -1,13 +1,10 @@
1
1
  import { accessSync, constants, readdirSync, statSync } from 'node:fs';
2
- import { delimiter, isAbsolute, resolve, sep } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { delimiter, isAbsolute, join, resolve, sep } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
- /**
5
- * Process-level helpers shared by the ACP and CLI runtime adapters: locating an
6
- * executable without a shell, discovering ACP servers on PATH, and reading a
7
- * structured answer out of an agent's stdout.
8
- */
5
+ import crossSpawn from 'cross-spawn';
9
6
  /** Package root, so bundled adapter binaries resolve in dev and after build. */
10
- const runtimePackageRoot = (() => {
7
+ export const runtimePackageRoot = (() => {
11
8
  const currentDirectory = fileURLToPath(new URL('.', import.meta.url));
12
9
  return currentDirectory.includes(`${sep}dist${sep}`)
13
10
  ? resolve(currentDirectory, '..', '..')
@@ -25,72 +22,183 @@ export const existingAbsoluteDirectory = (value, errorCode) => {
25
22
  }
26
23
  return resolve(value);
27
24
  };
28
- export const existingExecutable = (value) => {
25
+ export const existingExecutable = (value, platform = process.platform) => {
29
26
  try {
30
- if (statSync(value).isFile()) {
27
+ if (!statSync(value).isFile())
28
+ return undefined;
29
+ if (platform !== 'win32')
31
30
  accessSync(value, constants.X_OK);
32
- return value;
33
- }
31
+ return value;
34
32
  }
35
33
  catch {
36
34
  return undefined;
37
35
  }
38
- return undefined;
39
36
  };
40
- export const executableExtensions = () => {
41
- const raw = process.env.PATHEXT?.trim() || '.COM;.EXE;.BAT;.CMD;.PS1';
42
- return [
43
- ...new Set(raw
44
- .split(';')
45
- .map((extension) => extension.trim().toLowerCase())
46
- .filter((extension) => extension.startsWith('.'))),
47
- ];
37
+ const windowsExecutableExtensions = (env = process.env) => {
38
+ const configured = (env.PATHEXT ?? '.COM;.EXE;.CMD;.BAT')
39
+ .split(';')
40
+ .map((extension) => extension.trim().toLowerCase())
41
+ .filter((extension) => ['.com', '.exe', '.cmd', '.bat'].includes(extension));
42
+ return [...new Set([...configured, '.com', '.exe', '.cmd', '.bat'])];
48
43
  };
49
- export const stripExecutableExtension = (command) => {
44
+ export const executableExtensions = (platform = process.platform, env = process.env) => (platform === 'win32' ? windowsExecutableExtensions(env) : []);
45
+ export const stripExecutableExtension = (command, platform = process.platform, env = process.env) => {
50
46
  const lower = command.toLowerCase();
51
- const extension = executableExtensions()
47
+ const extension = executableExtensions(platform, env)
52
48
  .sort((a, b) => b.length - a.length)
53
49
  .find((candidate) => lower.endsWith(candidate));
54
50
  return extension ? command.slice(0, -extension.length) : command;
55
51
  };
56
- export const executableCandidates = (command) => {
52
+ export const executableCandidates = (command, platform = process.platform, env = process.env) => {
53
+ if (platform !== 'win32')
54
+ return [command];
57
55
  const lower = command.toLowerCase();
58
- if (executableExtensions().some((extension) => lower.endsWith(extension)))
56
+ if (/\.ps1$/i.test(lower))
57
+ return [];
58
+ if (executableExtensions(platform, env).some((extension) => lower.endsWith(extension)))
59
59
  return [command];
60
- return [command, ...executableExtensions().map((extension) => `${command}${extension}`)];
60
+ return executableExtensions(platform, env).map((extension) => `${command}${extension}`);
61
61
  };
62
- export const normalizedAcpCommand = (command) => {
63
- const normalized = stripExecutableExtension(command);
62
+ export const normalizedAcpCommand = (command, platform = process.platform, env = process.env) => {
63
+ const normalized = stripExecutableExtension(command, platform, env);
64
64
  return isAcpCommandName(normalized) ? normalized : undefined;
65
65
  };
66
- export const discoverAcpCommands = (projectRoot = process.cwd()) => {
67
- const projectBin = resolve(projectRoot, 'node_modules', '.bin');
68
- const bundledBin = resolve(runtimePackageRoot, 'node_modules', '.bin');
69
- const directories = [
70
- bundledBin,
71
- projectBin,
72
- ...(process.env.PATH ?? '')
73
- .split(delimiter)
66
+ const uniqueDirectories = (values) => {
67
+ const seen = new Set();
68
+ return values.filter(({ directory }) => {
69
+ const normalized = resolve(directory);
70
+ if (seen.has(normalized))
71
+ return false;
72
+ seen.add(normalized);
73
+ return true;
74
+ });
75
+ };
76
+ export const commandSearchDirectories = (options = {}) => {
77
+ const env = options.env ?? process.env;
78
+ const platform = options.platform ?? process.platform;
79
+ const packageRoot = resolve(options.bundledRoot ?? runtimePackageRoot);
80
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
81
+ const pathDelimiter = platform === 'win32' ? ';' : delimiter;
82
+ const values = [
83
+ { directory: join(packageRoot, 'node_modules', '.bin'), source: 'bundled-bin' },
84
+ { directory: join(projectRoot, 'node_modules', '.bin'), source: 'project-bin' },
85
+ ...(env.PATH ?? '')
86
+ .split(pathDelimiter)
74
87
  .filter(Boolean)
75
- .map((entry) => resolve(entry)),
88
+ .map((directory) => ({ directory, source: 'path' })),
76
89
  ];
90
+ if (platform === 'win32' && env.APPDATA)
91
+ values.push({ directory: join(env.APPDATA, 'npm'), source: 'windows-appdata' });
92
+ if (platform !== 'win32') {
93
+ const userHome = env.HOME ?? homedir();
94
+ values.push({ directory: join(userHome, '.local', 'bin'), source: 'common-path' }, { directory: join(userHome, '.cargo', 'bin'), source: 'common-path' }, { directory: join(userHome, '.npm-global', 'bin'), source: 'common-path' }, { directory: join(userHome, 'Library', 'pnpm'), source: 'common-path' }, { directory: '/opt/homebrew/bin', source: 'common-path' }, { directory: '/usr/local/bin', source: 'common-path' }, { directory: '/usr/bin', source: 'common-path' });
95
+ }
96
+ return uniqueDirectories(values);
97
+ };
98
+ export const resolveCommand = (command, options = {}) => {
99
+ const env = options.env ?? process.env;
100
+ const platform = options.platform ?? process.platform;
101
+ const launchType = (candidate) => platform === 'win32' && /\.(?:cmd|bat)$/i.test(candidate) ? 'windows-command' : 'native';
102
+ const direct = isAbsolute(command) || command.includes('/') || command.includes('\\');
103
+ if (direct) {
104
+ const target = isAbsolute(command)
105
+ ? command
106
+ : resolve(options.projectRoot ?? process.cwd(), command);
107
+ for (const candidate of executableCandidates(target, platform, env)) {
108
+ const executable = existingExecutable(candidate, platform);
109
+ if (executable)
110
+ return {
111
+ requested: command,
112
+ executable,
113
+ launchType: launchType(candidate),
114
+ source: 'explicit',
115
+ };
116
+ }
117
+ return undefined;
118
+ }
119
+ for (const { directory, source } of commandSearchDirectories(options)) {
120
+ for (const candidate of executableCandidates(command, platform, env)) {
121
+ const executable = existingExecutable(join(directory, candidate), platform);
122
+ if (executable)
123
+ return { requested: command, executable, launchType: launchType(candidate), source };
124
+ }
125
+ }
126
+ return undefined;
127
+ };
128
+ export const resolveCommandAliases = (commands, options = {}) => {
129
+ for (const command of commands) {
130
+ const resolved = resolveCommand(command, options);
131
+ if (resolved)
132
+ return resolved;
133
+ }
134
+ return undefined;
135
+ };
136
+ export const describeResolvedCommand = (command) => `${command.launchType} ${command.executable} (${command.source})`;
137
+ export const launchProcess = (spec) => crossSpawn(spec.resolved.executable, spec.args, {
138
+ cwd: spec.cwd,
139
+ env: spec.env,
140
+ stdio: spec.stdio,
141
+ shell: false,
142
+ windowsHide: true,
143
+ detached: process.platform !== 'win32',
144
+ });
145
+ const terminatingProcesses = new WeakSet();
146
+ export const terminateProcess = (child) => {
147
+ if (!child.pid ||
148
+ child.exitCode !== null ||
149
+ child.signalCode !== null ||
150
+ terminatingProcesses.has(child))
151
+ return;
152
+ terminatingProcesses.add(child);
153
+ if (process.platform === 'win32') {
154
+ const killer = crossSpawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
155
+ stdio: 'ignore',
156
+ windowsHide: true,
157
+ });
158
+ const fallback = () => {
159
+ if (child.exitCode === null && child.signalCode === null)
160
+ child.kill();
161
+ };
162
+ killer.once('error', fallback);
163
+ killer.once('exit', (code) => {
164
+ if (code !== 0)
165
+ fallback();
166
+ });
167
+ return;
168
+ }
169
+ try {
170
+ process.kill(-child.pid, 'SIGTERM');
171
+ }
172
+ catch {
173
+ child.kill('SIGTERM');
174
+ }
175
+ setTimeout(() => {
176
+ if (child.exitCode !== null || child.signalCode !== null || !child.pid)
177
+ return;
178
+ try {
179
+ process.kill(-child.pid, 'SIGKILL');
180
+ }
181
+ catch {
182
+ child.kill('SIGKILL');
183
+ }
184
+ }, 1_000).unref();
185
+ };
186
+ export const discoverAcpCommands = (options = {}) => {
187
+ const env = options.env ?? process.env;
188
+ const platform = options.platform ?? process.platform;
77
189
  const commands = new Set();
78
- const seen = new Set();
79
- for (const directory of directories) {
80
- if (seen.has(directory))
81
- continue;
82
- seen.add(directory);
190
+ for (const { directory } of commandSearchDirectories(options)) {
83
191
  try {
84
192
  for (const entry of readdirSync(directory)) {
85
- const command = normalizedAcpCommand(entry);
193
+ const command = normalizedAcpCommand(entry, platform, env);
86
194
  if (!command)
87
195
  continue;
88
- if (existingExecutable(resolve(directory, entry)))
196
+ if (resolveCommand(join(directory, entry), options))
89
197
  commands.add(command);
90
198
  }
91
199
  }
92
200
  catch {
93
- // Missing PATH entries are normal on developer machines.
201
+ // Missing search directories are normal on developer machines.
94
202
  }
95
203
  }
96
204
  return [...commands];
@@ -105,50 +213,6 @@ export const runtimeNameFromCommand = (command) => command
105
213
  .replace(/[-_.]+/g, ' ')
106
214
  .replace(/\bacp\b/gi, 'ACP')
107
215
  .replace(/\b\w/g, (letter) => letter.toUpperCase());
108
- export const resolveExecutable = (command, env, options = {}) => {
109
- if (isAbsolute(command)) {
110
- for (const candidate of executableCandidates(command)) {
111
- const executable = existingExecutable(candidate);
112
- if (executable)
113
- return executable;
114
- }
115
- return undefined;
116
- }
117
- if (command.includes(sep)) {
118
- for (const candidate of executableCandidates(resolve(command))) {
119
- const executable = existingExecutable(candidate);
120
- if (executable)
121
- return executable;
122
- }
123
- return undefined;
124
- }
125
- const localBins = [
126
- resolve(runtimePackageRoot, 'node_modules', '.bin'),
127
- resolve('node_modules', '.bin'),
128
- ];
129
- if (options.includeProjectBin) {
130
- for (const directory of localBins) {
131
- for (const candidate of executableCandidates(command)) {
132
- const localExecutable = existingExecutable(resolve(directory, candidate));
133
- if (localExecutable)
134
- return localExecutable;
135
- }
136
- }
137
- }
138
- for (const dir of (env.PATH ?? process.env.PATH ?? '').split(delimiter)) {
139
- if (!dir)
140
- continue;
141
- if (!options.includeProjectBin && localBins.includes(resolve(dir)))
142
- continue;
143
- for (const candidate of executableCandidates(command)) {
144
- const executable = existingExecutable(resolve(dir, candidate));
145
- if (executable)
146
- return executable;
147
- }
148
- }
149
- return undefined;
150
- };
151
- export const needsWindowsShell = (executable) => process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable);
152
216
  export const canChangeWorkspace = (effects) => effects.some((effect) => effect.type === 'file-write' || effect.type === 'command');
153
217
  export const parseJsonOutput = (text) => {
154
218
  const trimmed = text.trim();