@metagptx/deepflow-suite-cli 0.2.11-dev.20261020.1 → 0.3.1
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 +44 -50
- package/bin/deepflow-suite.js +4 -4
- package/bundle/deepflow-release.json +21 -46
- package/bundle/skills/deepflow-suite/SKILL.md +7 -1
- package/bundle/skills/df-agentctrl/SKILL.md +5 -1
- package/bundle/skills/df-agentrt/SKILL.md +15 -0
- package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
- package/bundle/skills/df-task/SKILL.md +4 -0
- package/bundle/skills/df-taskdriver/SKILL.md +5 -1
- package/bundle/skills/df-workbench/SKILL.md +10 -2
- package/manifest.json +12 -30
- package/package.json +8 -17
- package/runtime/df-agentctrl/commands.mjs +93 -0
- package/runtime/df-agentctrl/df-agentctrl.mjs +119 -0
- package/runtime/df-agentctrl/release.json +1 -0
- package/runtime/df-agentrt/agentrt.mjs +3 -0
- package/runtime/df-agentrt/df-agentrt.mjs +3 -0
- package/runtime/df-agentrt/release.json +1 -0
- package/runtime/df-agentrt/result.mjs +44 -0
- package/runtime/df-agentrt/runtime.mjs +358 -0
- package/runtime/df-agentrt/usage.mjs +16 -0
- package/runtime/df-task/commands.mjs +188 -0
- package/runtime/df-task/df-task.mjs +167 -0
- package/runtime/df-task/protocol.mjs +3 -0
- package/runtime/df-task/read.mjs +127 -0
- package/runtime/df-task/release.json +1 -0
- package/runtime/df-taskdriver/df-taskdriver.mjs +131 -0
- package/runtime/df-taskdriver/release.json +1 -0
- package/runtime/{df-workbench.mjs → df-workbench/df-workbench.mjs} +11 -2
- package/runtime/df-workbench/release.json +1 -0
- package/scripts/build-package.mjs +29 -25
- package/src/bundle.mjs +4 -1
- package/src/cli-install.mjs +35 -125
- package/src/commands/doctor.mjs +2 -0
- package/src/linker.mjs +2 -2
- package/src/manifest.mjs +7 -13
- package/src/package-cli.mjs +44 -60
- package/bin/df-agentctrl.js +0 -10
- package/bin/df-agentrt.js +0 -10
- package/bin/df-task.js +0 -10
- package/bin/df-taskdriver.js +0 -10
- package/bin/df-workbench.js +0 -10
- package/bundle/skills/deepflow-suite/manifest.yaml +0 -20
- package/bundle/skills/df-task/skill_test.go +0 -201
- package/bundle/skills/df-workbench/manifest.yaml +0 -5
- package/scripts/build-runtime-packages.mjs +0 -38
- package/skills/deepflow-suite/SKILL.md +0 -81
- package/skills/deepflow-suite/manifest.yaml +0 -20
- package/src/cli-launcher.mjs +0 -71
- package/src/runtime-build.mjs +0 -193
- package/src/runtime-package.mjs +0 -31
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const help = `df-agentctrl — Agent control plane CLI
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
df-agentctrl list [--include-disabled] [--metadata-source <source>] [--metadata-query-key <key>] [--json]
|
|
5
|
+
df-agentctrl get <agent-id> [--version <n>] [--metadata-source <source>] [--metadata-query-key <key>] [--json]
|
|
6
|
+
df-agentctrl create --file agent.json [--json]
|
|
7
|
+
df-agentctrl save <agent-id> --file agent.json [--change-note <text>] [--json]
|
|
8
|
+
df-agentctrl versions <agent-id> [--json]
|
|
9
|
+
df-agentctrl diff <agent-id> --from <n> --to <n> [--json]
|
|
10
|
+
df-agentctrl enable <agent-id> [--json]
|
|
11
|
+
df-agentctrl disable <agent-id> [--json]
|
|
12
|
+
df-agentctrl rollback <agent-id> --version <n> [--change-note <text>] [--json]
|
|
13
|
+
|
|
14
|
+
Global options (before or after the command):
|
|
15
|
+
--base-url <url> Service URL (or DF_AGENTCTRL_BASE_URL)
|
|
16
|
+
--json Machine-readable JSON output
|
|
17
|
+
help [command] Show general or command help (-h / --help also supported)
|
|
18
|
+
`;
|
|
19
|
+
export const commands = {
|
|
20
|
+
"list": {
|
|
21
|
+
"usage": "list [--include-disabled] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
22
|
+
"description": "List enabled agents and their default version summaries.",
|
|
23
|
+
"flags": [
|
|
24
|
+
"--include-disabled",
|
|
25
|
+
"--metadata-source",
|
|
26
|
+
"--metadata-query-key"
|
|
27
|
+
],
|
|
28
|
+
"needsID": false
|
|
29
|
+
},
|
|
30
|
+
"get": {
|
|
31
|
+
"usage": "get <agent-id> [--version <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
32
|
+
"description": "Read metadata and a complete definition; omit --version for the current default.",
|
|
33
|
+
"flags": [
|
|
34
|
+
"--version",
|
|
35
|
+
"--metadata-source",
|
|
36
|
+
"--metadata-query-key"
|
|
37
|
+
],
|
|
38
|
+
"needsID": true
|
|
39
|
+
},
|
|
40
|
+
"create": {
|
|
41
|
+
"usage": "create --file agent.json",
|
|
42
|
+
"description": "Create an agent and version 1 from a complete JSON snapshot.",
|
|
43
|
+
"flags": [
|
|
44
|
+
"--file"
|
|
45
|
+
],
|
|
46
|
+
"needsID": false
|
|
47
|
+
},
|
|
48
|
+
"save": {
|
|
49
|
+
"usage": "save <agent-id> --file agent.json [--change-note <text>]",
|
|
50
|
+
"description": "Save a complete snapshot as a new immutable default version.",
|
|
51
|
+
"flags": [
|
|
52
|
+
"--file",
|
|
53
|
+
"--change-note"
|
|
54
|
+
],
|
|
55
|
+
"needsID": true
|
|
56
|
+
},
|
|
57
|
+
"versions": {
|
|
58
|
+
"usage": "versions <agent-id>",
|
|
59
|
+
"description": "List immutable versions, newest first.",
|
|
60
|
+
"flags": [],
|
|
61
|
+
"needsID": true
|
|
62
|
+
},
|
|
63
|
+
"diff": {
|
|
64
|
+
"usage": "diff <agent-id> --from <n> --to <n>",
|
|
65
|
+
"description": "Compare two versions of the same agent as file diffs.",
|
|
66
|
+
"flags": [
|
|
67
|
+
"--from",
|
|
68
|
+
"--to"
|
|
69
|
+
],
|
|
70
|
+
"needsID": true
|
|
71
|
+
},
|
|
72
|
+
"enable": {
|
|
73
|
+
"usage": "enable <agent-id>",
|
|
74
|
+
"description": "Include an agent in the default directory.",
|
|
75
|
+
"flags": [],
|
|
76
|
+
"needsID": true
|
|
77
|
+
},
|
|
78
|
+
"disable": {
|
|
79
|
+
"usage": "disable <agent-id>",
|
|
80
|
+
"description": "Exclude an agent from the default directory; preserve its history.",
|
|
81
|
+
"flags": [],
|
|
82
|
+
"needsID": true
|
|
83
|
+
},
|
|
84
|
+
"rollback": {
|
|
85
|
+
"usage": "rollback <agent-id> --version <n> [--change-note <text>]",
|
|
86
|
+
"description": "Copy a historical definition into a new default version; preserve name and enabled state.",
|
|
87
|
+
"flags": [
|
|
88
|
+
"--version",
|
|
89
|
+
"--change-note"
|
|
90
|
+
],
|
|
91
|
+
"needsID": true
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { help, commands } from './commands.mjs';
|
|
6
|
+
|
|
7
|
+
const uuid = /^(?:urn:uuid:)?(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})$/i;
|
|
8
|
+
const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags)]);
|
|
9
|
+
export function parse(args) {
|
|
10
|
+
const o = { command: '', args: [], values: {}, json: false, help: false, includeDisabled: false };
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (!arg.startsWith('-')) { if (!o.command) o.command = arg; else o.args.push(arg); continue; }
|
|
15
|
+
const equals = arg.indexOf('=');
|
|
16
|
+
const key = equals < 0 ? arg : arg.slice(0, equals);
|
|
17
|
+
if (seen.has(key)) throw Error(`duplicate option ${key}`);
|
|
18
|
+
seen.add(key);
|
|
19
|
+
if (['--help', '-h', '--json', '--include-disabled'].includes(key)) {
|
|
20
|
+
if (equals >= 0) throw Error(`${key} does not take a value`);
|
|
21
|
+
o[key === '--json' ? 'json' : key === '--include-disabled' ? 'includeDisabled' : 'help'] = true;
|
|
22
|
+
} else if (valueFlags.has(key)) {
|
|
23
|
+
if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
|
|
24
|
+
o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
|
|
25
|
+
} else throw Error(`unknown option ${key}`);
|
|
26
|
+
}
|
|
27
|
+
o.command ||= 'help';
|
|
28
|
+
if (o.command === 'help' && o.args.length === 1) { o.command = o.args.pop(); o.help = true; }
|
|
29
|
+
return o;
|
|
30
|
+
}
|
|
31
|
+
function query(values) {
|
|
32
|
+
const pairs = Object.entries(values).filter(([, value]) => value !== undefined && value !== '');
|
|
33
|
+
pairs.sort(([a], [b]) => a.localeCompare(b));
|
|
34
|
+
const value = new URLSearchParams(pairs).toString();
|
|
35
|
+
return value ? `?${value}` : '';
|
|
36
|
+
}
|
|
37
|
+
export async function requestFor(o) {
|
|
38
|
+
const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined, v = o.values;
|
|
39
|
+
if (!spec) throw Error(`unknown command ${o.command}`);
|
|
40
|
+
for (const key of Object.keys(v)) if (key !== '--base-url' && !spec.flags.includes(key)) throw Error(`${key} is not supported by ${o.command}`);
|
|
41
|
+
if (o.includeDisabled && !spec.flags.includes('--include-disabled')) throw Error('--include-disabled is only supported by list');
|
|
42
|
+
if (spec.needsID) {
|
|
43
|
+
if (o.args.length !== 1) throw Error(`${o.command} requires exactly one agent-id`);
|
|
44
|
+
if (!uuid.test(o.args[0])) throw Error('agent-id must be a UUID');
|
|
45
|
+
} else if (o.args.length) throw Error(`${o.command} does not accept positional arguments`);
|
|
46
|
+
for (const key of ['--version', '--from', '--to']) if (key in v) {
|
|
47
|
+
if (!/^\+?\d+$/.test(v[key]) || BigInt(v[key]) < 1n || BigInt(v[key]) > 2147483647n) throw Error(`${key} must be a positive 32-bit integer`);
|
|
48
|
+
}
|
|
49
|
+
const metadata = { metadata_source: v['--metadata-source'], metadata_query_key: v['--metadata-query-key'] };
|
|
50
|
+
let path = '/api/v1/agents';
|
|
51
|
+
if (o.command === 'list') return { method: 'GET', path: path + query({ ...metadata, ...(o.includeDisabled ? { include_disabled: 'true' } : {}) }) };
|
|
52
|
+
if (spec.needsID) path += `/${encodeURIComponent(o.args[0])}`;
|
|
53
|
+
if (['create', 'save'].includes(o.command)) {
|
|
54
|
+
if (!v['--file']) throw Error('--file is required');
|
|
55
|
+
const raw = await readFile(v['--file'], 'utf8');
|
|
56
|
+
let obj;
|
|
57
|
+
try { obj = JSON.parse(raw); } catch (error) { throw Error(`invalid JSON: ${error.message}`); }
|
|
58
|
+
if (!obj || Array.isArray(obj) || typeof obj !== 'object') throw Error('snapshot must be a JSON object');
|
|
59
|
+
// Inject the note without rounding numbers or discarding unknown snapshot fields.
|
|
60
|
+
const trimmed = raw.trim();
|
|
61
|
+
const body = '--change-note' in v ? trimmed.slice(0, -1) + (Object.keys(obj).length ? ',' : '') + `"change_note":${JSON.stringify(v['--change-note'])}}` : raw;
|
|
62
|
+
return { method: 'POST', path: path + (o.command === 'save' ? '/versions' : ''), body };
|
|
63
|
+
}
|
|
64
|
+
if (o.command === 'get') return { method: 'GET', path: path + (v['--version'] ? `/versions/${encodeURIComponent(v['--version'])}` : '') + query(metadata) };
|
|
65
|
+
if (o.command === 'versions') return { method: 'GET', path: `${path}/versions` };
|
|
66
|
+
if (['enable', 'disable'].includes(o.command)) return { method: 'POST', path: `${path}/${o.command}` };
|
|
67
|
+
if (o.command === 'diff') {
|
|
68
|
+
if (!v['--from'] || !v['--to']) throw Error('--from and --to are required');
|
|
69
|
+
return { method: 'GET', path: `${path}/versions/${encodeURIComponent(v['--to'])}/diff${query({ against: v['--from'] })}` };
|
|
70
|
+
}
|
|
71
|
+
if (!v['--version']) throw Error('--version is required');
|
|
72
|
+
return { method: 'POST', path: `${path}/versions/${encodeURIComponent(v['--version'])}/rollback`, body: JSON.stringify({ change_note: v['--change-note'] || '' }) };
|
|
73
|
+
}
|
|
74
|
+
function pretty(raw) {
|
|
75
|
+
const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
|
|
76
|
+
let level = 0, out = '';
|
|
77
|
+
const newline = () => '\n' + ' '.repeat(level);
|
|
78
|
+
tokens.forEach((token, i) => {
|
|
79
|
+
if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
|
|
80
|
+
else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
|
|
81
|
+
else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
|
|
82
|
+
});
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
86
|
+
if (args[0] === '--cli-version') {
|
|
87
|
+
let version = 'workspace';
|
|
88
|
+
try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
|
|
89
|
+
const report = { name: 'df-agentctrl', version, entry: fileURLToPath(import.meta.url) };
|
|
90
|
+
stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-agentctrl ${version}`) + '\n');
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
let o, base, req;
|
|
94
|
+
try {
|
|
95
|
+
o = parse(args);
|
|
96
|
+
if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
|
|
97
|
+
if (o.help) {
|
|
98
|
+
const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined; if (!spec) throw Error(`unknown command ${o.command}`);
|
|
99
|
+
stdout.write(`df-agentctrl ${spec.usage} [--base-url <url>] [--json]\n\n${spec.description}\n`); return 0;
|
|
100
|
+
}
|
|
101
|
+
req = await requestFor(o);
|
|
102
|
+
base = o.values['--base-url'] || env.DF_AGENTCTRL_BASE_URL || 'http://127.0.0.1:8080';
|
|
103
|
+
const url = new URL(base);
|
|
104
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
|
|
105
|
+
} catch (error) { stderr.write(error.message + '\n'); return 2; }
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers: req.body ? { 'Content-Type': 'application/json' } : {}, signal: AbortSignal.timeout(30000) });
|
|
108
|
+
const data = await response.text();
|
|
109
|
+
if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
|
|
110
|
+
let value; try { value = JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
|
|
111
|
+
if (o.json) stdout.write(data + '\n');
|
|
112
|
+
else if (o.command === 'diff' && (value === null || (typeof value === 'object' && !Array.isArray(value) && (value.files == null || Array.isArray(value.files))))) {
|
|
113
|
+
for (const file of value?.files || []) stdout.write(`${file.path} (+${file.additions} -${file.deletions})\n${file.patch}`);
|
|
114
|
+
if (!value?.files?.length) stdout.write('No differences.\n');
|
|
115
|
+
} else stdout.write(pretty(data) + '\n');
|
|
116
|
+
return 0;
|
|
117
|
+
} catch (error) { stderr.write(error.message + '\n'); return 1; }
|
|
118
|
+
}
|
|
119
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-agent-ctrl","sourceCommit":"10e745773559f4712b04ab61915a018995e30630","sourceDirty":false}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-agent-runtime","sourceCommit":"420ebb91ed4cf92ca81eb8c295cc111f94b9da2c","sourceDirty":false}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdtemp, open, link, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const terminal = (status) => ['succeeded', 'failed', 'cancelled'].includes(status);
|
|
6
|
+
const statuses = ['pending', 'waiting_for_runtime', 'claimed', 'running', 'succeeded', 'failed', 'cancelled'];
|
|
7
|
+
export function resultView(inv, maxBytes) {
|
|
8
|
+
const view = { invocation_id: inv.id, session_id: inv.session_id, status: inv.status, terminal: terminal(inv.status), result_state: terminal(inv.status) ? 'missing' : 'pending', output_present: false, output: '', output_bytes: 0, output_returned_bytes: 0, output_truncated: false };
|
|
9
|
+
if (inv.finished_at) view.finished_at = inv.finished_at;
|
|
10
|
+
if (inv.error) view.error = inv.error;
|
|
11
|
+
const invalid = (message) => { view.result_state = 'invalid'; view.format_error = message; return { view, output: '' }; };
|
|
12
|
+
if (!statuses.includes(inv.status)) { view.result_state = 'invalid'; view.format_error = 'unknown invocation status'; }
|
|
13
|
+
if (!inv.result) return { view, output: '' };
|
|
14
|
+
let result;
|
|
15
|
+
try { result = JSON.parse(inv.result); } catch { return invalid('result is not a JSON object'); }
|
|
16
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) return invalid('result is not a JSON object');
|
|
17
|
+
for (const [field, target] of [['error', 'result_error'], ['failure_reason', 'failure_reason']]) {
|
|
18
|
+
if (!Object.hasOwn(result, field) || result[field] === null) continue;
|
|
19
|
+
if (typeof result[field] !== 'string') return invalid(`${field} is not a string`);
|
|
20
|
+
if (result[field]) view[target] = result[field];
|
|
21
|
+
}
|
|
22
|
+
if (!Object.hasOwn(result, 'output') || result.output === null) return { view, output: '' };
|
|
23
|
+
if (typeof result.output !== 'string') return invalid('output is not a string');
|
|
24
|
+
const bytes = Buffer.from(result.output);
|
|
25
|
+
view.output_present = true; view.output_bytes = bytes.length;
|
|
26
|
+
if (view.terminal) view.result_state = 'available';
|
|
27
|
+
view.output_sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
28
|
+
let end = Math.min(maxBytes, bytes.length);
|
|
29
|
+
if (end < bytes.length) while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
30
|
+
view.output = bytes.subarray(0, end).toString('utf8');
|
|
31
|
+
view.output_truncated = bytes.length > maxBytes;
|
|
32
|
+
view.output_returned_bytes = end;
|
|
33
|
+
return { view, output: result.output };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Publish a private file atomically without replacing a concurrent destination.
|
|
37
|
+
export async function writeResultFile(destination, data) {
|
|
38
|
+
const directory = await mkdtemp(path.join(path.dirname(destination), '.invocation-result-'));
|
|
39
|
+
try {
|
|
40
|
+
const staging = path.join(directory, 'output'), file = await open(staging, 'wx', 0o600);
|
|
41
|
+
try { await file.writeFile(data); } finally { await file.close(); }
|
|
42
|
+
await link(staging, destination);
|
|
43
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
44
|
+
}
|