@metagptx/deepflow-suite-cli 0.2.10 → 0.3.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.
Files changed (58) hide show
  1. package/README.md +44 -50
  2. package/bin/deepflow-suite.js +4 -4
  3. package/bundle/deepflow-release.json +21 -91
  4. package/bundle/skills/deepflow-suite/SKILL.md +7 -1
  5. package/bundle/skills/df-agentctrl/SKILL.md +92 -10
  6. package/bundle/skills/df-agentrt/SKILL.md +65 -14
  7. package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
  8. package/bundle/skills/df-task/SKILL.md +98 -26
  9. package/bundle/skills/df-taskdriver/SKILL.md +8 -4
  10. package/bundle/skills/df-workbench/SKILL.md +5 -9
  11. package/manifest.json +12 -30
  12. package/package.json +8 -17
  13. package/runtime/df-agentctrl/commands.mjs +93 -0
  14. package/runtime/df-agentctrl/df-agentctrl.mjs +119 -0
  15. package/runtime/df-agentctrl/release.json +1 -0
  16. package/runtime/df-agentrt/agentrt.mjs +3 -0
  17. package/runtime/df-agentrt/df-agentrt.mjs +3 -0
  18. package/runtime/df-agentrt/release.json +1 -0
  19. package/runtime/df-agentrt/runtime.mjs +326 -0
  20. package/runtime/df-agentrt/usage.mjs +16 -0
  21. package/runtime/df-task/commands.mjs +181 -0
  22. package/runtime/df-task/df-task.mjs +155 -0
  23. package/runtime/df-task/protocol.mjs +3 -0
  24. package/runtime/df-task/release.json +1 -0
  25. package/runtime/df-taskdriver/df-taskdriver.mjs +131 -0
  26. package/runtime/df-taskdriver/release.json +1 -0
  27. package/runtime/{df-workbench.mjs → df-workbench/df-workbench.mjs} +17 -175
  28. package/runtime/df-workbench/release.json +1 -0
  29. package/scripts/build-package.mjs +29 -25
  30. package/src/bundle.mjs +4 -1
  31. package/src/cli-install.mjs +35 -125
  32. package/src/commands/doctor.mjs +2 -0
  33. package/src/linker.mjs +2 -2
  34. package/src/manifest.mjs +7 -13
  35. package/src/package-cli.mjs +44 -60
  36. package/bin/df-agentctrl.js +0 -10
  37. package/bin/df-agentrt.js +0 -10
  38. package/bin/df-task.js +0 -10
  39. package/bin/df-taskdriver.js +0 -10
  40. package/bin/df-workbench.js +0 -10
  41. package/bundle/skills/deepflow-suite/manifest.yaml +0 -20
  42. package/bundle/skills/df-agentctrl/references/environment.md +0 -40
  43. package/bundle/skills/df-agentctrl/references/query-scope.md +0 -50
  44. package/bundle/skills/df-agentrt/references/environment.md +0 -29
  45. package/bundle/skills/df-agentrt/references/invocation-result.md +0 -11
  46. package/bundle/skills/df-agentrt/references/query-scope.md +0 -30
  47. package/bundle/skills/df-agentrt/references/session-create.md +0 -14
  48. package/bundle/skills/df-task/references/environment.md +0 -50
  49. package/bundle/skills/df-task/references/query-scope.md +0 -49
  50. package/bundle/skills/df-task/references/reading.md +0 -9
  51. package/bundle/skills/df-task/skill_test.go +0 -201
  52. package/bundle/skills/df-workbench/manifest.yaml +0 -5
  53. package/scripts/build-runtime-packages.mjs +0 -38
  54. package/skills/deepflow-suite/SKILL.md +0 -81
  55. package/skills/deepflow-suite/manifest.yaml +0 -20
  56. package/src/cli-launcher.mjs +0 -71
  57. package/src/runtime-build.mjs +0 -193
  58. package/src/runtime-package.mjs +0 -31
@@ -0,0 +1,181 @@
1
+ export const help = `df-task — DeepFlow Task fact log CLI
2
+
3
+ Usage:
4
+ df-task ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
5
+ df-task get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]
6
+ df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>]
7
+ df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
8
+ df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
9
+ df-task stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
10
+ df-task title <task-id> --title <text>
11
+ df-task metadata <task-id> --metadata <json> [--metadata-source <source>] [--metadata-query-key <key>]
12
+
13
+ Scheduling shorthands (each one is a wrapper around append):
14
+ df-task dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]
15
+ df-task wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]
16
+ df-task handoff <task-id> --ask <text> [--assignee <who>]
17
+
18
+ Global options (before or after the command):
19
+ --base-url <url> Service URL (or DF_TASK_BASE_URL)
20
+ --json Machine-readable JSON output
21
+ help [command] Show general or command help (-h / --help also supported)
22
+
23
+ Environment:
24
+ DF_TASK_BASE_URL Service URL, default http://127.0.0.1:8090
25
+ DF_TASK_ACTOR_KIND Write identity: human | external | task_run | scheduler_run | driver
26
+ DF_TASK_ACTOR_ID Write identity id, e.g. the current run id
27
+
28
+ Facts are append-only. The API has no way to change or remove a fact, so neither does this CLI.
29
+ `;
30
+ export const commands = {
31
+ "ls": {
32
+ "usage": "ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
33
+ "description": "List tasks, newest first.",
34
+ "flags": [
35
+ "--limit",
36
+ "--metadata-source",
37
+ "--metadata-query-key"
38
+ ],
39
+ "required": [],
40
+ "needsID": false,
41
+ "writes": false
42
+ },
43
+ "get": {
44
+ "usage": "get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]",
45
+ "description": "Read one task's identity.",
46
+ "flags": [
47
+ "--metadata-source",
48
+ "--metadata-query-key"
49
+ ],
50
+ "required": [],
51
+ "needsID": true,
52
+ "writes": false
53
+ },
54
+ "create": {
55
+ "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>]",
56
+ "description": "Create a task, optionally with its first user message in the same transaction.",
57
+ "flags": [
58
+ "--title",
59
+ "--description",
60
+ "--message",
61
+ "--metadata"
62
+ ],
63
+ "required": [
64
+ "--title"
65
+ ],
66
+ "needsID": false,
67
+ "writes": true
68
+ },
69
+ "facts": {
70
+ "usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
71
+ "description": "Read one task's facts in seq order.",
72
+ "flags": [
73
+ "--after",
74
+ "--types",
75
+ "--limit",
76
+ "--metadata-source",
77
+ "--metadata-query-key"
78
+ ],
79
+ "required": [],
80
+ "needsID": true,
81
+ "writes": false
82
+ },
83
+ "append": {
84
+ "usage": "append <task-id> --type <type> --file payload.json [--idempotency-key <k>]",
85
+ "description": "Append one fact from a JSON payload file.",
86
+ "flags": [
87
+ "--type",
88
+ "--file",
89
+ "--idempotency-key"
90
+ ],
91
+ "required": [
92
+ "--type",
93
+ "--file"
94
+ ],
95
+ "needsID": true,
96
+ "writes": true
97
+ },
98
+ "stream": {
99
+ "usage": "stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
100
+ "description": "Read facts across all tasks from a cursor. One-shot pull, not a long connection.",
101
+ "flags": [
102
+ "--after",
103
+ "--types",
104
+ "--limit",
105
+ "--metadata-source",
106
+ "--metadata-query-key"
107
+ ],
108
+ "required": [],
109
+ "needsID": false,
110
+ "writes": false
111
+ },
112
+ "title": {
113
+ "usage": "title <task-id> --title <text>",
114
+ "description": "Change a task's title. Writes no fact.",
115
+ "flags": [
116
+ "--title"
117
+ ],
118
+ "required": [
119
+ "--title"
120
+ ],
121
+ "needsID": true,
122
+ "writes": true
123
+ },
124
+ "metadata": {
125
+ "usage": "metadata <task-id> --metadata <json> [--metadata-source <source>] [--metadata-query-key <key>]",
126
+ "description": "Replace a task's metadata as human. Writes no fact.",
127
+ "flags": [
128
+ "--metadata",
129
+ "--metadata-source",
130
+ "--metadata-query-key"
131
+ ],
132
+ "required": [
133
+ "--metadata"
134
+ ],
135
+ "needsID": true,
136
+ "writes": true
137
+ },
138
+ "dispatch": {
139
+ "usage": "dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]",
140
+ "description": "Append a dispatch intent. Without --run it asks for a new run; with --run it posts a message to that run.",
141
+ "flags": [
142
+ "--agent",
143
+ "--instruction",
144
+ "--run"
145
+ ],
146
+ "required": [
147
+ "--agent",
148
+ "--instruction"
149
+ ],
150
+ "needsID": true,
151
+ "writes": true
152
+ },
153
+ "wait": {
154
+ "usage": "wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]",
155
+ "description": "Append a waiting reason. It is context for humans and the next scheduling run, not a gate for the driver.",
156
+ "flags": [
157
+ "--on",
158
+ "--reason",
159
+ "--waiting-run"
160
+ ],
161
+ "required": [
162
+ "--on",
163
+ "--reason"
164
+ ],
165
+ "needsID": true,
166
+ "writes": true
167
+ },
168
+ "handoff": {
169
+ "usage": "handoff <task-id> --ask <text> [--assignee <who>]",
170
+ "description": "Append a handoff to a human. Handing the ball back and asking a human to do a step are the same fact.",
171
+ "flags": [
172
+ "--ask",
173
+ "--assignee"
174
+ ],
175
+ "required": [
176
+ "--ask"
177
+ ],
178
+ "needsID": true,
179
+ "writes": true
180
+ }
181
+ };
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { realpathSync } from 'node:fs';
5
+ import { help, commands } from './commands.mjs';
6
+ import { factTypes, actorKinds } from './protocol.mjs';
7
+
8
+ 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;
9
+ const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags)]);
10
+ export function parse(args) {
11
+ const o = { command: '', args: [], values: {}, json: false, help: false };
12
+ const seen = new Set();
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (!arg.startsWith('-')) { if (!o.command) o.command = arg; else o.args.push(arg); continue; }
16
+ const equals = arg.indexOf('=');
17
+ const key = equals < 0 ? arg : arg.slice(0, equals);
18
+ if (seen.has(key)) throw Error(`duplicate option ${key}`);
19
+ seen.add(key);
20
+ if (['--help', '-h', '--json'].includes(key)) {
21
+ if (equals >= 0) throw Error(`${key} does not take a value`);
22
+ o[key === '--json' ? 'json' : 'help'] = true;
23
+ } else if (valueFlags.has(key)) {
24
+ if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
25
+ o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
26
+ } else throw Error(`unknown option ${key}`);
27
+ }
28
+ o.command ||= 'help';
29
+ if (o.command === 'help' && o.args.length === 1) { o.command = o.args.pop(); o.help = true; }
30
+ return o;
31
+ }
32
+
33
+ export function validate(o, env) {
34
+ const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined;
35
+ if (!spec) throw Error(`unknown command ${o.command}`);
36
+ for (const key of Object.keys(o.values)) if (key !== '--base-url' && !spec.flags.includes(key)) throw Error(`${key} is not supported by ${o.command}`);
37
+ for (const key of spec.required) if (!o.values[key]?.trim()) throw Error(`${o.command} requires ${key}`);
38
+ if (spec.needsID) {
39
+ if (o.args.length !== 1) throw Error(`${o.command} requires exactly one task-id`);
40
+ if (!uuid.test(o.args[0])) throw Error('task-id must be a UUID');
41
+ } else if (o.args.length) throw Error(`${o.command} does not accept positional arguments`);
42
+ for (const key of ['--type', '--types']) if (key in o.values) {
43
+ for (const raw of o.values[key].split(',')) {
44
+ const name = raw.trim();
45
+ if (!name) throw Error('--types must not contain an empty entry');
46
+ if (!factTypes.includes(name)) throw Error(`unknown fact type ${name}`);
47
+ }
48
+ }
49
+ if ('--run' in o.values && !o.values['--run'].trim()) throw Error('--run must not be empty');
50
+ if ('--metadata' in o.values) object(o.values['--metadata'], '--metadata must be a JSON object');
51
+ for (const key of ['--limit', '--after']) if (key in o.values) {
52
+ const raw = o.values[key];
53
+ if (!/^[+]?\d+$/.test(raw) || BigInt(raw) > 9223372036854775807n || (key === '--limit' && BigInt(raw) === 0n)) throw Error(`${key} must be a non-negative integer`);
54
+ }
55
+ if (spec.writes) {
56
+ if (!env.DF_TASK_ACTOR_KIND?.trim() || !env.DF_TASK_ACTOR_ID?.trim()) throw Error('writing a fact requires DF_TASK_ACTOR_KIND and DF_TASK_ACTOR_ID');
57
+ if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
58
+ if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
59
+ }
60
+ }
61
+ function object(raw, message) {
62
+ let value;
63
+ try { value = JSON.parse(raw); } catch { throw Error(message); }
64
+ if (!value || Array.isArray(value) || typeof value !== 'object') throw Error(message);
65
+ return value;
66
+ }
67
+ function query(values) {
68
+ const pairs = Object.entries(values).filter(([, value]) => value !== undefined && value !== '');
69
+ pairs.sort(([a], [b]) => a.localeCompare(b));
70
+ const q = new URLSearchParams(pairs).toString();
71
+ return q ? `?${q}` : '';
72
+ }
73
+ export async function requestFor(o) {
74
+ const v = o.values, c = o.command;
75
+ const path = o.args.length ? `/api/v1/tasks/${encodeURIComponent(o.args[0])}` : '/api/v1/tasks';
76
+ const metadata = { metadata_source: v['--metadata-source'], metadata_query_key: v['--metadata-query-key'] };
77
+ const post = (p, body) => ({ method: 'POST', path: p, body: JSON.stringify(body) });
78
+ if (c === 'ls') return { method: 'GET', path: path + query({ ...metadata, limit: v['--limit'] || '50' }) };
79
+ if (c === 'get') return { method: 'GET', path: path + query(metadata) };
80
+ if (c === 'facts' || c === 'stream') return { method: 'GET', path: (c === 'facts' ? `${path}/facts` : '/api/v1/facts') + query({ ...metadata, after_seq: v['--after'], types: v['--types'], limit: v['--limit'] }) };
81
+ // Preserve user JSON bytes, including integer precision and unknown fields.
82
+ if (c === 'create') return { method: 'POST', path, body: `{"title":${JSON.stringify(v['--title'])},"description":${JSON.stringify(v['--description'] || '')}${v['--message'] ? `,"first_message":${JSON.stringify(v['--message'])}` : ''}${v['--metadata'] ? `,"metadata":${v['--metadata']}` : ''}}` };
83
+ if (c === 'title') return post(`${path}/title`, { title: v['--title'] });
84
+ if (c === 'metadata') return { method: 'POST', path: `${path}/metadata${query(metadata)}`, body: `{"metadata":${v['--metadata']}}` };
85
+ if (c === 'append') {
86
+ const raw = await readFile(v['--file'], 'utf8');
87
+ object(raw, `${v['--file']} must contain a JSON object`);
88
+ return { method: 'POST', path: `${path}/facts`, body: `{"type":${JSON.stringify(v['--type'])},"payload":${raw}${v['--idempotency-key'] ? `,"idempotency_key":${JSON.stringify(v['--idempotency-key'])}` : ''}}` };
89
+ }
90
+ let type, payload;
91
+ if (c === 'dispatch') { type = 'dispatch_intent'; payload = { agent: v['--agent'], instruction: v['--instruction'], mode: v['--run'] ? 'message' : 'new_run', ...(v['--run'] ? { target_run_id: v['--run'] } : {}) }; }
92
+ if (c === 'wait') { type = 'waiting_reason'; payload = { waiting_on: v['--on'], reason: v['--reason'], ...(v['--waiting-run'] ? { waiting_run_id: v['--waiting-run'] } : {}) }; }
93
+ if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}) }; }
94
+ return post(`${path}/facts`, { type, payload });
95
+ }
96
+ function human(command, data, stdout) {
97
+ const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
98
+ if (typeof page !== 'object' || Array.isArray(page) || (page.tasks != null && !Array.isArray(page.tasks)) || (page.facts != null && !Array.isArray(page.facts))) { stdout.write(pretty(data) + '\n'); return; }
99
+ if (command === 'ls') {
100
+ for (const task of page.tasks || []) stdout.write(`${task.id} ${task.updated_at} ${task.title}\n`);
101
+ if (!page.tasks?.length) stdout.write('No tasks.\n');
102
+ } else if (command === 'facts' || command === 'stream') {
103
+ for (const f of page.facts || []) {
104
+ const payload = Array.from(JSON.stringify(f.payload, (_key, value) => typeof value === 'bigint' ? JSON.rawJSON(value.toString()) : value));
105
+ const text = payload.length > 120 ? payload.slice(0, 117).join('') + '...' : payload.join('');
106
+ stdout.write(`${String(f.seq).padEnd(8)} ${f.created_at || ''} ${f.task_id ? f.task_id + ' ' : ''}${(f.type || '').padEnd(20)} ${f.actor_kind || ''}:${f.actor_id || ''} ${text}\n`);
107
+ }
108
+ stdout.write(`next_after_seq=${page.next_after_seq || 0}${command === 'stream' ? ` max_seq=${page.max_seq || 0}` : ''}\n`);
109
+ } else stdout.write(pretty(data) + '\n');
110
+ }
111
+ function pretty(raw) {
112
+ const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
113
+ let level = 0, out = '';
114
+ const newline = () => '\n' + ' '.repeat(level);
115
+ tokens.forEach((token, i) => {
116
+ if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
117
+ else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
118
+ else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
119
+ });
120
+ return out;
121
+ }
122
+ export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
123
+ if (args[0] === '--cli-version') {
124
+ let version = 'workspace';
125
+ try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
126
+ const report = { name: 'df-task', version, entry: fileURLToPath(import.meta.url) };
127
+ stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-task ${version}`) + '\n');
128
+ return 0;
129
+ }
130
+ let o, base, req;
131
+ try {
132
+ o = parse(args);
133
+ if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
134
+ if (o.help) {
135
+ const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined; if (!spec) throw Error(`unknown command ${o.command}`);
136
+ stdout.write(`df-task ${spec.usage} [--base-url <url>] [--json]\n\n${spec.description}\n`); return 0;
137
+ }
138
+ validate(o, env);
139
+ base = o.values['--base-url'] || env.DF_TASK_BASE_URL || 'http://127.0.0.1:8090';
140
+ const url = new URL(base);
141
+ if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
142
+ req = await requestFor(o);
143
+ } catch (error) { stderr.write(error.message + '\n'); return 2; }
144
+ try {
145
+ const headers = req.body ? { 'Content-Type': 'application/json' } : {};
146
+ if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
147
+ const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
148
+ const data = await response.text();
149
+ if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
150
+ try { JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
151
+ if (o.json) stdout.write(data + '\n'); else human(o.command, data, stdout);
152
+ return 0;
153
+ } catch (error) { stderr.write(error.message + '\n'); return 1; }
154
+ }
155
+ if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
@@ -0,0 +1,3 @@
1
+ // Generated by generate-protocol.mjs from internal/model/fact.go.
2
+ export const factTypes = ["task_lifecycle","user_message","external_result","artifact_published","summary","help_requested","run_outcome","dispatch_intent","waiting_reason","handoff_to_human","adjudication","run_terminated","invocation_started","invocation_accepted","invocation_ended","attempt_failed"];
3
+ export const actorKinds = ["human","external","task_run","scheduler_run","driver"];
@@ -0,0 +1 @@
1
+ {"version":"0.3.0","project":"deepflow-task","sourceCommit":"3a8f5c8915cf5df74236eb4da693ac75f4c816a8","sourceDirty":false}
@@ -0,0 +1,131 @@
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
+
6
+ const commands = { health: '存活探针:进程还在吗', ready: '就绪探针:能不能干活(未就绪时退出码 1)', locks: '当前哪些 Task 正在被处理、被谁', task: '这个 Task 为什么不动', metrics: '抓取指标,可按子串过滤', help: '打印这段用法' };
7
+ export function displayWidth(text) {
8
+ return Array.from(text).reduce((width, char) => { const r = char.codePointAt(0); return width + ((r >= 0x1100 && r <= 0x115f) || (r >= 0x2e80 && r <= 0xa4cf) || (r >= 0xac00 && r <= 0xd7a3) || (r >= 0xf900 && r <= 0xfaff) || (r >= 0xfe30 && r <= 0xfe4f) || (r >= 0xff00 && r <= 0xff60) || (r >= 0xffe0 && r <= 0xffe6) ? 2 : 1); }, 0);
9
+ }
10
+ export const pad = (text, width) => text + ' '.repeat(Math.max(1, width - displayWidth(text)));
11
+ function usage(out) {
12
+ out.write('df-taskdriver —— deepflow-task-driver 运维 CLI\n\n用法: df-taskdriver [选项] <子命令> [参数]\n\n子命令:\n');
13
+ for (const [name, description] of Object.entries(commands)) out.write(` ${pad(name === 'task' ? 'task <task-id> [limit]' : name === 'metrics' ? 'metrics [过滤子串]' : name, 26)} ${description}\n`);
14
+ out.write('\n选项:\n --base-url string driver 实例的 ops 地址\n --timeout duration 单次请求超时 (default 10s)\n --json 原样输出实例返回的 JSON\n\n退出码: 0 肯定 / 1 实例给出否定回答 / 2 连不上或用法错误\n');
15
+ }
16
+ function duration(raw) {
17
+ if (raw === '0') return 0;
18
+ let total = 0, consumed = '';
19
+ for (const match of raw.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|μs|ms|s|m|h)/g)) {
20
+ consumed += match[0]; total += Number(match[1]) * { ns: 1e-6, us: 1e-3, 'µs': 1e-3, 'μs': 1e-3, ms: 1, s: 1000, m: 60000, h: 3600000 }[match[2]];
21
+ }
22
+ if (!consumed || consumed !== raw || !Number.isFinite(total)) throw Error(`invalid duration ${raw}`);
23
+ return total;
24
+ }
25
+ export function parse(args, env) {
26
+ const options = { args: [], base: env.DF_TASKDRIVER_BASE_URL || 'http://127.0.0.1:8081', timeout: 10000, json: false };
27
+ for (let i = 0; i < args.length; i++) {
28
+ if (!args[i].startsWith('-') || args[i] === '-') { options.args.push(args[i]); continue; }
29
+ if (args[i] === '--') { options.args.push(...args.slice(i + 1)); break; }
30
+ const [key, inline] = args[i].replace(/^-{1,2}/, '').split(/=(.*)/s);
31
+ if (key === 'help' || key === 'h') { options.help = true; break; }
32
+ if (key === 'json') { if (inline !== undefined && !['true', 'false'].includes(inline)) throw Error('invalid boolean value'); options.json = inline !== 'false'; continue; }
33
+ if (!['base-url', 'timeout'].includes(key)) throw Error(`flag provided but not defined: -${key}`);
34
+ const value = inline ?? args[++i]; if (value === undefined) throw Error(`flag needs an argument: -${key}`);
35
+ if (key === 'base-url') options.base = value; else options.timeout = duration(value);
36
+ }
37
+ return options;
38
+ }
39
+ const humanMS = (ms) => ms < 0 ? '未知' : `${(Number(ms || 0) / 1000).toFixed(1)}s`;
40
+ const compact = (payload) => Object.keys(payload || {}).sort().map((key) => `${key}=${typeof payload[key] === 'object' ? JSON.stringify(payload[key]) : payload[key]}`).join(' ') || '{}';
41
+ // Format tokens rather than re-encoding parsed numbers: seq values are int64.
42
+ function pretty(raw) {
43
+ const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
44
+ let level = 0, out = '';
45
+ const newline = () => '\n' + ' '.repeat(level);
46
+ tokens.forEach((token, i) => {
47
+ if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
48
+ else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
49
+ else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
50
+ });
51
+ return out;
52
+ }
53
+ function human(command, body, status, out) {
54
+ const b = body || {};
55
+ if (command === 'health') out.write(`存活: ${b.status || ''} instance=${b.instance_id || ''}\n`);
56
+ if (command === 'ready') out.write(status === 200 ? '就绪\n' : `未就绪: ${b.error || ''}\n`);
57
+ if (command === 'locks') {
58
+ if (status !== 200) out.write(`读取失败: ${b.error || ''}\n`);
59
+ else if (!b.count) out.write('当前没有任何 Task 被持锁\n');
60
+ else {
61
+ out.write(`${b.count} 个 Task 正在被处理\n`);
62
+ for (const lock of b.locks || []) out.write(` ${lock.task_id} holder=${lock.holder} (${lock.recognized ? '本实例' : '其他实例'}) 剩余 ${humanMS(lock.remaining_ttl_ms)}\n`);
63
+ }
64
+ }
65
+ if (command === 'task') {
66
+ if (status !== 200) { out.write(`读取失败: ${b.error || ''}\n`); return; }
67
+ const d = b.decision || {};
68
+ out.write(`${b.task_id || ''}\n 结论: ${d.should_schedule ? '下一轮会调度' : '不会调度'} —— ${d.reason || ''}\n 事实: ${b.facts || 0} 条,max_seq=${b.max_seq || 0}\n`);
69
+ out.write(` 水位线: ${d.max_watermark || 0},最大触发事实: ${d.max_trigger_seq || 0},本轮锚点: ${d.anchor || 0}\n`);
70
+ out.write(` 失败预算: 组 ${d.budget_group || 0} 内 ${d.failures_in_budget_group || 0}/${d.failure_budget || 0} 次失败${d.budget_exhausted ? '(已耗尽,等人介入;发一条新的用户消息即可恢复)' : ''}\n`);
71
+ if (b.inflight?.length) {
72
+ out.write(' 在飞唤起:\n');
73
+ for (const inv of b.inflight) out.write(` ${inv.run_id} kind=${inv.run_kind} started_seq=${inv.started_seq || 0} intent_seq=${inv.intent_seq || 0} 水位线=${inv.watermark || 0} 锚点=${inv.anchor || 0}${inv.needs_replay ? ' [待重放]' : ''}\n`);
74
+ }
75
+ if (b.unfulfilled_intents?.length) {
76
+ out.write(' 未兑现意图:\n');
77
+ for (const intent of b.unfulfilled_intents) out.write(` seq=${intent.seq} ${compact(intent.payload)}\n`);
78
+ }
79
+ out.write(b.inflight?.length || b.unfulfilled_intents?.length ? ' 驱动这边还有未了之事(见上方在飞唤起/未兑现意图),先看它们卡在哪一步\n' : d.should_schedule ? ' 驱动这边没有未了之事,谓词已成立,下一拍就会起一轮调度——这一刻不用做什么\n' : ' 驱动无未了之事,继续查上游(事实是否写入、调度智能体是否下发)\n');
80
+ }
81
+ }
82
+ export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
83
+ if (args[0] === '--cli-version') {
84
+ let version = 'workspace';
85
+ try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
86
+ const report = { name: 'df-taskdriver', version, entry: fileURLToPath(import.meta.url) };
87
+ stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-taskdriver ${version}`) + '\n');
88
+ return 0;
89
+ }
90
+ let o, command, rest, path;
91
+ try {
92
+ o = parse(args, env); [command, ...rest] = o.args;
93
+ if (o.help || command === 'help') { usage(stdout); return 0; }
94
+ if (!command || !Object.hasOwn(commands, command)) { if (command) stderr.write(`未知子命令: ${command}\n\n`); usage(stderr); return 2; }
95
+ path = { health: '/healthz', ready: '/readyz', locks: '/debug/locks', metrics: '/metrics' }[command];
96
+ if (command === 'task') {
97
+ if (!rest.length) throw Error('用法: df-taskdriver task <task-id> [limit]');
98
+ if (rest[1] !== undefined && (!/^\+?\d+$/.test(rest[1]) || BigInt(rest[1]) < 1n || BigInt(rest[1]) > 9223372036854775807n)) throw Error(`limit 必须是正整数: ${rest[1]}`);
99
+ path = `/debug/tasks/${encodeURIComponent(rest[0])}${rest[1] !== undefined ? `?limit=${encodeURIComponent(rest[1])}` : ''}`;
100
+ }
101
+ } catch (error) { stderr.write(`错误: ${error.message}\n`); return 2; }
102
+ const controller = new AbortController();
103
+ const stop = () => controller.abort();
104
+ process.once('SIGINT', stop); process.once('SIGTERM', stop);
105
+ const timer = o.timeout ? setTimeout(stop, o.timeout) : undefined;
106
+ try {
107
+ const response = await fetch(o.base.replace(/\/+$/, '') + path, { signal: controller.signal });
108
+ const chunks = []; let length = 0;
109
+ for await (const chunk of response.body) { length += chunk.length; if (length > 8 * 1024 * 1024) throw Error('response exceeds 8 MiB'); chunks.push(chunk); }
110
+ const data = Buffer.concat(chunks).toString('utf8'), affirmative = response.status >= 200 && response.status < 300;
111
+ let body, valid = true;
112
+ if (command !== 'metrics') { try { body = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value); } catch { valid = false; } }
113
+ if (command === 'metrics') {
114
+ stdout.write(rest.length ? data.split('\n').filter((line) => line.includes(rest[0])).join('\n') + '\n' : data);
115
+ return affirmative ? 0 : 1;
116
+ }
117
+ if (o.json) {
118
+ // Raw server JSON preserves unknown fields and integer precision.
119
+ stdout.write(valid ? pretty(data) + '\n' : data + (data.endsWith('\n') ? '' : '\n'));
120
+ return affirmative ? valid ? 0 : 2 : 1;
121
+ }
122
+ if (!valid || (body !== null && (typeof body !== 'object' || Array.isArray(body)))) {
123
+ stderr.write(`错误: 解析 ${path} 的响应失败(HTTP ${response.status},响应体前 200 字节: ${Array.from(data.trim()).slice(0, 200).join('')})\n`);
124
+ return affirmative ? 2 : 1;
125
+ }
126
+ human(command, body, response.status, stdout);
127
+ return command === 'ready' ? response.status === 200 ? 0 : 1 : affirmative ? 0 : 1;
128
+ } catch (error) { stderr.write(`错误: 请求 ${o.base} 失败: ${error.message}(检查 --base-url / DF_TASKDRIVER_BASE_URL)\n`); return 2; }
129
+ finally { clearTimeout(timer); process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); }
130
+ }
131
+ 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.0","project":"deepflow-task-driver","sourceCommit":"ab582505c8d60a99c8d2939c3d58b31803a8a229","sourceDirty":false}