@hmj-ai/cflow 1.1.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,244 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
5
+ const builtinManifests = [
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' },
16
+ },
17
+ {
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'],
26
+ },
27
+ ];
28
+ const manifestHash = (manifest) => createHash('sha256').update(JSON.stringify(manifest)).digest('hex').slice(0, 16);
29
+ const stringValue = (value, name, max) => {
30
+ if (typeof value !== 'string' || !value.trim() || value.length > max)
31
+ throw new Error(`${name}_INVALID`);
32
+ if (value.includes('\0'))
33
+ throw new Error(`${name}_INVALID`);
34
+ return value.trim();
35
+ };
36
+ const stringList = (value, name, maxItems) => {
37
+ if (value === undefined)
38
+ return undefined;
39
+ if (!Array.isArray(value) || value.length > maxItems)
40
+ throw new Error(`${name}_INVALID`);
41
+ return value.map((item) => stringValue(item, name, 500));
42
+ };
43
+ const numberValue = (value, name, minimum, maximum) => {
44
+ if (value === undefined)
45
+ return undefined;
46
+ const parsed = Number(value);
47
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum)
48
+ throw new Error(`${name}_INVALID`);
49
+ return parsed;
50
+ };
51
+ const traitValue = (traits, key, allowed) => {
52
+ const value = traits[key];
53
+ if (value === undefined)
54
+ return undefined;
55
+ if (!allowed.includes(value))
56
+ throw new Error('TRAITS_INVALID');
57
+ return value;
58
+ };
59
+ const normalizeTraits = (value) => {
60
+ if (value === undefined)
61
+ return undefined;
62
+ if (!value || typeof value !== 'object' || Array.isArray(value))
63
+ throw new Error('TRAITS_INVALID');
64
+ const traits = value;
65
+ const normalized = {
66
+ backendKind: traitValue(traits, 'backendKind', ['acp', 'process']),
67
+ sessionMode: traitValue(traits, 'sessionMode', ['stateless', 'per-cf-call', 'persistent']),
68
+ structuredOutput: traitValue(traits, 'structuredOutput', [true, false]),
69
+ streaming: traitValue(traits, 'streaming', [true, false]),
70
+ toolEvents: traitValue(traits, 'toolEvents', [true, false]),
71
+ permissionPrompts: traitValue(traits, 'permissionPrompts', [true, false]),
72
+ tokenAccounting: traitValue(traits, 'tokenAccounting', ['exact', 'approximate', 'unavailable']),
73
+ cancellation: traitValue(traits, 'cancellation', [
74
+ 'cooperative',
75
+ 'process-kill',
76
+ 'unsupported',
77
+ ]),
78
+ filesystemIsolation: traitValue(traits, 'filesystemIsolation', [
79
+ 'sandboxed',
80
+ 'cwd-scoped',
81
+ 'host-permissions',
82
+ ]),
83
+ networkIsolation: traitValue(traits, 'networkIsolation', [
84
+ 'enforced',
85
+ 'adapter-declared',
86
+ 'unenforced',
87
+ ]),
88
+ };
89
+ return Object.fromEntries(Object.entries(normalized).filter(([, item]) => item !== undefined));
90
+ };
91
+ const normalizePermissionArgs = (value) => {
92
+ if (value === undefined)
93
+ return undefined;
94
+ if (!value || typeof value !== 'object' || Array.isArray(value))
95
+ throw new Error('PERMISSION_ARGS_INVALID');
96
+ const input = value;
97
+ const output = {};
98
+ for (const mode of ['none', 'read', 'write', 'full']) {
99
+ const args = stringList(input[mode], 'PERMISSION_ARGS', 30);
100
+ if (args)
101
+ output[mode] = args;
102
+ }
103
+ return output;
104
+ };
105
+ const normalizeManifest = (value, baseDirectory) => {
106
+ if (!value || typeof value !== 'object' || Array.isArray(value))
107
+ throw new Error('OBJECT_INVALID');
108
+ const input = value;
109
+ if (input.schemaVersion !== 1)
110
+ throw new Error('SCHEMA_VERSION_UNSUPPORTED');
111
+ const id = stringValue(input.id, 'ID', 64).toLowerCase();
112
+ if (!/^[a-z0-9][a-z0-9._-]{1,63}$/.test(id))
113
+ throw new Error('ID_INVALID');
114
+ const backend = input.backend;
115
+ if (backend !== 'acp' && backend !== 'cli')
116
+ throw new Error('BACKEND_INVALID');
117
+ const rawCommand = stringValue(input.command, 'COMMAND', 500);
118
+ const command = baseDirectory &&
119
+ !isAbsolute(rawCommand) &&
120
+ (rawCommand.startsWith('./') || rawCommand.startsWith('../'))
121
+ ? resolve(baseDirectory, rawCommand)
122
+ : rawCommand;
123
+ const promptTransport = input.promptTransport;
124
+ if (promptTransport !== undefined &&
125
+ promptTransport !== 'stdin' &&
126
+ promptTransport !== 'argument')
127
+ throw new Error('PROMPT_TRANSPORT_INVALID');
128
+ const outputMode = input.outputMode;
129
+ if (outputMode !== undefined && outputMode !== 'json' && outputMode !== 'text')
130
+ throw new Error('OUTPUT_MODE_INVALID');
131
+ const envAllowlist = stringList(input.envAllowlist, 'ENV_ALLOWLIST', 40);
132
+ if (envAllowlist?.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)))
133
+ throw new Error('ENV_ALLOWLIST_INVALID');
134
+ return {
135
+ schemaVersion: 1,
136
+ id,
137
+ name: stringValue(input.name, 'NAME', 80),
138
+ description: input.description === undefined
139
+ ? undefined
140
+ : stringValue(input.description, 'DESCRIPTION', 400),
141
+ backend,
142
+ command,
143
+ args: stringList(input.args, 'ARGS', 40),
144
+ versionArgs: stringList(input.versionArgs, 'VERSION_ARGS', 10),
145
+ promptTransport,
146
+ outputMode,
147
+ timeoutMs: numberValue(input.timeoutMs, 'TIMEOUT', 1_000, 3_600_000),
148
+ maxOutputBytes: numberValue(input.maxOutputBytes, 'OUTPUT_LIMIT', 1_024, 16_777_216),
149
+ envAllowlist,
150
+ capabilities: stringList(input.capabilities, 'CAPABILITIES', 40),
151
+ permissionArgs: normalizePermissionArgs(input.permissionArgs),
152
+ traits: normalizeTraits(input.traits),
153
+ };
154
+ };
155
+ const record = (value, source, manifestPath, baseDirectory) => {
156
+ const manifest = normalizeManifest(value, baseDirectory);
157
+ return { manifest, source, manifestPath, manifestHash: manifestHash(manifest) };
158
+ };
159
+ const manifestFiles = (directory) => {
160
+ if (!directory || !existsSync(directory))
161
+ return [];
162
+ return readdirSync(directory)
163
+ .filter((name) => name.endsWith('.json'))
164
+ .sort()
165
+ .map((name) => join(directory, name));
166
+ };
167
+ const packageJsonFiles = (packageRoot) => {
168
+ if (!packageRoot || !existsSync(packageRoot))
169
+ return [];
170
+ const files = [];
171
+ for (const name of readdirSync(packageRoot).sort()) {
172
+ if (name === '.bin' || name.startsWith('.'))
173
+ continue;
174
+ const candidate = join(packageRoot, name);
175
+ try {
176
+ if (name.startsWith('@') && statSync(candidate).isDirectory()) {
177
+ for (const child of readdirSync(candidate).sort())
178
+ files.push(join(candidate, child, 'package.json'));
179
+ }
180
+ else
181
+ files.push(join(candidate, 'package.json'));
182
+ }
183
+ catch {
184
+ // Broken package links are ignored during discovery.
185
+ }
186
+ }
187
+ return files.filter(existsSync);
188
+ };
189
+ export function loadAgentManifestRecords(options = {}) {
190
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
191
+ const userDirectory = options.userManifestDirectory === false
192
+ ? false
193
+ : resolve(options.userManifestDirectory ??
194
+ join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'cflow', 'agents.d'));
195
+ const projectDirectory = options.projectManifestDirectory === false
196
+ ? false
197
+ : resolve(options.projectManifestDirectory ?? join(projectRoot, '.cflow', 'agents.d'));
198
+ const packageRoot = options.packageRoot === false
199
+ ? false
200
+ : resolve(options.packageRoot ?? join(projectRoot, 'node_modules'));
201
+ const records = [];
202
+ const warnings = [];
203
+ if (options.includeBuiltins !== false)
204
+ for (const manifest of builtinManifests)
205
+ records.push(record(manifest, 'builtin', `builtin:${manifest.id}`));
206
+ const addValues = (values, source, manifestPath, baseDirectory) => {
207
+ const manifests = Array.isArray(values) ? values : [values];
208
+ for (const value of manifests) {
209
+ try {
210
+ records.push(record(value, source, manifestPath, baseDirectory));
211
+ }
212
+ catch (error) {
213
+ warnings.push(`${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
214
+ }
215
+ }
216
+ };
217
+ for (const packageJson of packageJsonFiles(packageRoot)) {
218
+ try {
219
+ const value = JSON.parse(readFileSync(packageJson, 'utf8'));
220
+ if (value.cflowAgent !== undefined)
221
+ addValues(value.cflowAgent, 'package-manifest', packageJson, dirname(packageJson));
222
+ }
223
+ catch {
224
+ // A package without readable metadata is unrelated unless it advertises cflowAgent.
225
+ }
226
+ }
227
+ for (const [directory, source] of [
228
+ [userDirectory, 'user-manifest'],
229
+ [projectDirectory, 'project-manifest'],
230
+ ]) {
231
+ for (const manifestPath of manifestFiles(directory)) {
232
+ try {
233
+ addValues(JSON.parse(readFileSync(manifestPath, 'utf8')), source, manifestPath, dirname(manifestPath));
234
+ }
235
+ catch (error) {
236
+ warnings.push(`${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
237
+ }
238
+ }
239
+ }
240
+ const selected = new Map();
241
+ for (const candidate of records)
242
+ selected.set(candidate.manifest.id, candidate);
243
+ return { records: [...selected.values()], warnings };
244
+ }
@@ -0,0 +1,175 @@
1
+ import { accessSync, constants, readdirSync, statSync } from 'node:fs';
2
+ import { delimiter, isAbsolute, resolve, sep } from 'node:path';
3
+ 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
+ */
9
+ /** Package root, so bundled adapter binaries resolve in dev and after build. */
10
+ const runtimePackageRoot = (() => {
11
+ const currentDirectory = fileURLToPath(new URL('.', import.meta.url));
12
+ return currentDirectory.includes(`${sep}dist${sep}`)
13
+ ? resolve(currentDirectory, '..', '..')
14
+ : resolve(currentDirectory, '..');
15
+ })();
16
+ export const existingAbsoluteDirectory = (value, errorCode) => {
17
+ if (!isAbsolute(value))
18
+ throw new Error(errorCode);
19
+ try {
20
+ if (!statSync(value).isDirectory())
21
+ throw new Error(errorCode);
22
+ }
23
+ catch {
24
+ throw new Error(errorCode);
25
+ }
26
+ return resolve(value);
27
+ };
28
+ export const existingExecutable = (value) => {
29
+ try {
30
+ if (statSync(value).isFile()) {
31
+ accessSync(value, constants.X_OK);
32
+ return value;
33
+ }
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ return undefined;
39
+ };
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
+ ];
48
+ };
49
+ export const stripExecutableExtension = (command) => {
50
+ const lower = command.toLowerCase();
51
+ const extension = executableExtensions()
52
+ .sort((a, b) => b.length - a.length)
53
+ .find((candidate) => lower.endsWith(candidate));
54
+ return extension ? command.slice(0, -extension.length) : command;
55
+ };
56
+ export const executableCandidates = (command) => {
57
+ const lower = command.toLowerCase();
58
+ if (executableExtensions().some((extension) => lower.endsWith(extension)))
59
+ return [command];
60
+ return [command, ...executableExtensions().map((extension) => `${command}${extension}`)];
61
+ };
62
+ export const normalizedAcpCommand = (command) => {
63
+ const normalized = stripExecutableExtension(command);
64
+ return isAcpCommandName(normalized) ? normalized : undefined;
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)
74
+ .filter(Boolean)
75
+ .map((entry) => resolve(entry)),
76
+ ];
77
+ 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);
83
+ try {
84
+ for (const entry of readdirSync(directory)) {
85
+ const command = normalizedAcpCommand(entry);
86
+ if (!command)
87
+ continue;
88
+ if (existingExecutable(resolve(directory, entry)))
89
+ commands.add(command);
90
+ }
91
+ }
92
+ catch {
93
+ // Missing PATH entries are normal on developer machines.
94
+ }
95
+ }
96
+ return [...commands];
97
+ };
98
+ export const isAcpCommandName = (command) => /^acp-[a-z0-9._-]+$/i.test(command) || /^[a-z0-9._-]+-acp$/i.test(command);
99
+ export const runtimeIdFromCommand = (command) => command
100
+ .toLowerCase()
101
+ .replace(/[^a-z0-9._-]+/g, '-')
102
+ .replace(/^-+|-+$/g, '')
103
+ .slice(0, 64);
104
+ export const runtimeNameFromCommand = (command) => command
105
+ .replace(/[-_.]+/g, ' ')
106
+ .replace(/\bacp\b/gi, 'ACP')
107
+ .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
+ export const canChangeWorkspace = (effects) => effects.some((effect) => effect.type === 'file-write' || effect.type === 'command');
153
+ export const parseJsonOutput = (text) => {
154
+ const trimmed = text.trim();
155
+ const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed)?.[1] ?? trimmed;
156
+ try {
157
+ return JSON.parse(fenced);
158
+ }
159
+ catch {
160
+ const start = Math.min(...['{', '['].map((token) => {
161
+ const index = fenced.indexOf(token);
162
+ return index < 0 ? Number.POSITIVE_INFINITY : index;
163
+ }));
164
+ const end = Math.max(fenced.lastIndexOf('}'), fenced.lastIndexOf(']'));
165
+ if (Number.isFinite(start) && end > start) {
166
+ try {
167
+ return JSON.parse(fenced.slice(start, end + 1));
168
+ }
169
+ catch {
170
+ // Fall through to a loud structured-output error.
171
+ }
172
+ }
173
+ throw new Error('RUNTIME_JSON_INVALID');
174
+ }
175
+ };