@metagptx/deepflow-suite-cli 0.3.0 → 0.3.2
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/bundle/deepflow-release.json +61 -16
- package/bundle/skills/df-agentctrl/SKILL.md +9 -87
- package/bundle/skills/df-agentctrl/references/environment.md +40 -0
- package/bundle/skills/df-agentctrl/references/query-scope.md +50 -0
- package/bundle/skills/df-agentrt/SKILL.md +25 -61
- package/bundle/skills/df-agentrt/references/environment.md +29 -0
- package/bundle/skills/df-agentrt/references/invocation-result.md +11 -0
- package/bundle/skills/df-agentrt/references/query-scope.md +30 -0
- package/bundle/skills/df-agentrt/references/session-create.md +14 -0
- package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
- package/bundle/skills/df-task/SKILL.md +30 -92
- package/bundle/skills/df-task/references/environment.md +50 -0
- package/bundle/skills/df-task/references/fact-types.md +29 -3
- package/bundle/skills/df-task/references/query-scope.md +49 -0
- package/bundle/skills/df-task/references/reading.md +9 -0
- package/bundle/skills/df-taskdriver/SKILL.md +3 -3
- package/bundle/skills/df-workbench/SKILL.md +13 -1
- package/manifest.json +2 -1
- package/package.json +1 -1
- package/runtime/df-agentctrl/release.json +1 -1
- package/runtime/df-agentrt/release.json +1 -1
- package/runtime/df-agentrt/result.mjs +44 -0
- package/runtime/df-agentrt/runtime.mjs +36 -4
- package/runtime/df-agentrt/usage.mjs +2 -2
- package/runtime/df-task/commands.mjs +23 -12
- package/runtime/df-task/df-task.mjs +34 -8
- package/runtime/df-task/protocol.mjs +1 -1
- package/runtime/df-task/read.mjs +127 -0
- package/runtime/df-task/release.json +1 -1
- package/runtime/df-taskdriver/release.json +1 -1
- package/runtime/df-workbench/df-workbench.mjs +274 -14
- package/runtime/df-workbench/release.json +1 -1
|
@@ -2,9 +2,10 @@ export const help = `df-task — DeepFlow Task fact log CLI
|
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
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>]
|
|
5
|
+
df-task get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]
|
|
6
|
+
df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]
|
|
7
7
|
df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
|
|
8
|
+
df-task facts <task-id> --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]
|
|
8
9
|
df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
|
|
9
10
|
df-task stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
|
|
10
11
|
df-task title <task-id> --title <text>
|
|
@@ -13,7 +14,7 @@ Usage:
|
|
|
13
14
|
Scheduling shorthands (each one is a wrapper around append):
|
|
14
15
|
df-task dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]
|
|
15
16
|
df-task wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]
|
|
16
|
-
df-task handoff <task-id> --ask <text> [--assignee <who>]
|
|
17
|
+
df-task handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]
|
|
17
18
|
|
|
18
19
|
Global options (before or after the command):
|
|
19
20
|
--base-url <url> Service URL (or DF_TASK_BASE_URL)
|
|
@@ -22,7 +23,7 @@ Global options (before or after the command):
|
|
|
22
23
|
|
|
23
24
|
Environment:
|
|
24
25
|
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_KIND Write identity: human | bot | external | task_run | scheduler_run | driver
|
|
26
27
|
DF_TASK_ACTOR_ID Write identity id, e.g. the current run id
|
|
27
28
|
|
|
28
29
|
Facts are append-only. The API has no way to change or remove a fact, so neither does this CLI.
|
|
@@ -41,9 +42,10 @@ export const commands = {
|
|
|
41
42
|
"writes": false
|
|
42
43
|
},
|
|
43
44
|
"get": {
|
|
44
|
-
"usage": "get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
45
|
-
"description": "Read one task's identity.",
|
|
45
|
+
"usage": "get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
46
|
+
"description": "Read one task's identity. --fields selects known fields and object paths; absent paths are omitted.",
|
|
46
47
|
"flags": [
|
|
48
|
+
"--fields",
|
|
47
49
|
"--metadata-source",
|
|
48
50
|
"--metadata-query-key"
|
|
49
51
|
],
|
|
@@ -52,13 +54,14 @@ export const commands = {
|
|
|
52
54
|
"writes": false
|
|
53
55
|
},
|
|
54
56
|
"create": {
|
|
55
|
-
"usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>]",
|
|
57
|
+
"usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]",
|
|
56
58
|
"description": "Create a task, optionally with its first user message in the same transaction.",
|
|
57
59
|
"flags": [
|
|
58
60
|
"--title",
|
|
59
61
|
"--description",
|
|
60
62
|
"--message",
|
|
61
|
-
"--metadata"
|
|
63
|
+
"--metadata",
|
|
64
|
+
"--idempotency-key"
|
|
62
65
|
],
|
|
63
66
|
"required": [
|
|
64
67
|
"--title"
|
|
@@ -67,9 +70,14 @@ export const commands = {
|
|
|
67
70
|
"writes": true
|
|
68
71
|
},
|
|
69
72
|
"facts": {
|
|
70
|
-
"usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
71
|
-
"description": "Read
|
|
73
|
+
"usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--fields <paths> --json | --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
74
|
+
"description": "Read facts in seq order. --fields selects fields per fact, retaining pagination. --all writes complete raw pages to a private file and prints a manifest; limits default to 10 pages / 8 MiB. Only an observed empty page marks complete. Partial reads exit 1.",
|
|
72
75
|
"flags": [
|
|
76
|
+
"--fields",
|
|
77
|
+
"--all",
|
|
78
|
+
"--output-file",
|
|
79
|
+
"--max-pages",
|
|
80
|
+
"--max-bytes",
|
|
73
81
|
"--after",
|
|
74
82
|
"--types",
|
|
75
83
|
"--limit",
|
|
@@ -166,11 +174,14 @@ export const commands = {
|
|
|
166
174
|
"writes": true
|
|
167
175
|
},
|
|
168
176
|
"handoff": {
|
|
169
|
-
"usage": "handoff <task-id> --ask <text> [--assignee <who>]",
|
|
177
|
+
"usage": "handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]",
|
|
170
178
|
"description": "Append a handoff to a human. Handing the ball back and asking a human to do a step are the same fact.",
|
|
171
179
|
"flags": [
|
|
172
180
|
"--ask",
|
|
173
|
-
"--assignee"
|
|
181
|
+
"--assignee",
|
|
182
|
+
"--participation",
|
|
183
|
+
"--task-assignee",
|
|
184
|
+
"--idempotency-key"
|
|
174
185
|
],
|
|
175
186
|
"required": [
|
|
176
187
|
"--ask"
|
|
@@ -4,9 +4,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
5
|
import { help, commands } from './commands.mjs';
|
|
6
6
|
import { factTypes, actorKinds } from './protocol.mjs';
|
|
7
|
+
import { validateReadOptions, projectResponse, readAllFacts, readResponse } from './read.mjs';
|
|
7
8
|
|
|
8
9
|
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
|
+
const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags).filter((flag) => flag !== '--all')]);
|
|
10
11
|
export function parse(args) {
|
|
11
12
|
const o = { command: '', args: [], values: {}, json: false, help: false };
|
|
12
13
|
const seen = new Set();
|
|
@@ -20,6 +21,9 @@ export function parse(args) {
|
|
|
20
21
|
if (['--help', '-h', '--json'].includes(key)) {
|
|
21
22
|
if (equals >= 0) throw Error(`${key} does not take a value`);
|
|
22
23
|
o[key === '--json' ? 'json' : 'help'] = true;
|
|
24
|
+
} else if (key === '--all') {
|
|
25
|
+
if (equals >= 0) throw Error(`${key} does not take a value`);
|
|
26
|
+
o.values[key] = 'true';
|
|
23
27
|
} else if (valueFlags.has(key)) {
|
|
24
28
|
if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
|
|
25
29
|
o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
|
|
@@ -54,9 +58,23 @@ export function validate(o, env) {
|
|
|
54
58
|
}
|
|
55
59
|
if (spec.writes) {
|
|
56
60
|
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');
|
|
61
|
+
if (o.command === 'create') {
|
|
62
|
+
const key = o.values['--idempotency-key'] || '';
|
|
63
|
+
if (key.length > 200 || /[^\x21-\x7e]/.test(key) || (env.DF_TASK_ACTOR_KIND === 'bot' && key.length < 8)) throw Error('creation idempotency key must be at most 200 visible ASCII bytes; bot creation requires at least 8 bytes');
|
|
64
|
+
}
|
|
57
65
|
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
|
|
66
|
+
if (o.command === 'metadata' && !['human','bot'].includes(env.DF_TASK_ACTOR_KIND)) throw Error('metadata requires DF_TASK_ACTOR_KIND=human or bot');
|
|
67
|
+
}
|
|
68
|
+
if (o.command === 'handoff' && ('--participation' in o.values || '--task-assignee' in o.values)) {
|
|
69
|
+
const target = o.values['--assignee'];
|
|
70
|
+
if (o.values['--participation'] !== 'ensure_member') throw Error('--participation must be ensure_member for member operations');
|
|
71
|
+
if (!target || target.trim() !== target || Buffer.byteLength(target) > 200) throw Error('member operations require a non-empty --assignee of at most 200 bytes without surrounding whitespace');
|
|
72
|
+
if ('--task-assignee' in o.values) {
|
|
73
|
+
const assignment = object(o.values['--task-assignee'], '--task-assignee must be a JSON object');
|
|
74
|
+
if (Object.keys(assignment).length !== 2 || assignment.kind !== 'human' || assignment.id !== target) throw Error('--task-assignee must identify the same human as --assignee');
|
|
75
|
+
}
|
|
59
76
|
}
|
|
77
|
+
validateReadOptions(o);
|
|
60
78
|
}
|
|
61
79
|
function object(raw, message) {
|
|
62
80
|
let value;
|
|
@@ -90,8 +108,8 @@ export async function requestFor(o) {
|
|
|
90
108
|
let type, payload;
|
|
91
109
|
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
110
|
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 });
|
|
111
|
+
if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}), ...(v['--participation'] ? { participation: v['--participation'] } : {}), ...(v['--task-assignee'] ? { task_assignee: object(v['--task-assignee'], '--task-assignee must be a JSON object') } : {}) }; }
|
|
112
|
+
return post(`${path}/facts`, { type, payload, ...(v['--idempotency-key'] ? {idempotency_key:v['--idempotency-key']} : {}) });
|
|
95
113
|
}
|
|
96
114
|
function human(command, data, stdout) {
|
|
97
115
|
const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
|
|
@@ -127,7 +145,7 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
|
|
|
127
145
|
stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-task ${version}`) + '\n');
|
|
128
146
|
return 0;
|
|
129
147
|
}
|
|
130
|
-
let o, base,
|
|
148
|
+
let o, base, initialRequest;
|
|
131
149
|
try {
|
|
132
150
|
o = parse(args);
|
|
133
151
|
if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
|
|
@@ -139,15 +157,23 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
|
|
|
139
157
|
base = o.values['--base-url'] || env.DF_TASK_BASE_URL || 'http://127.0.0.1:8090';
|
|
140
158
|
const url = new URL(base);
|
|
141
159
|
if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
|
|
142
|
-
|
|
160
|
+
initialRequest = await requestFor(o);
|
|
143
161
|
} catch (error) { stderr.write(error.message + '\n'); return 2; }
|
|
144
|
-
|
|
162
|
+
const request = async (options, limit = 0) => {
|
|
163
|
+
const req = options === o ? initialRequest : await requestFor(options);
|
|
145
164
|
const headers = req.body ? { 'Content-Type': 'application/json' } : {};
|
|
165
|
+
if (o.command === 'create' && o.values['--idempotency-key']) headers['Idempotency-Key'] = o.values['--idempotency-key'];
|
|
146
166
|
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
167
|
const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
|
|
148
|
-
const data = await response
|
|
168
|
+
const data = await readResponse(response, limit);
|
|
149
169
|
if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
|
|
150
170
|
try { JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
|
|
171
|
+
return data;
|
|
172
|
+
};
|
|
173
|
+
if (o.values['--all']) return readAllFacts(o, base, request, stdout, stderr);
|
|
174
|
+
try {
|
|
175
|
+
let data = await request(o, '--fields' in o.values ? 64 << 20 : 0);
|
|
176
|
+
if ('--fields' in o.values) data = projectResponse(o.command, data, o.values['--fields'].split(','));
|
|
151
177
|
if (o.json) stdout.write(data + '\n'); else human(o.command, data, stdout);
|
|
152
178
|
return 0;
|
|
153
179
|
} catch (error) { stderr.write(error.message + '\n'); return 1; }
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by generate-protocol.mjs from internal/model/fact.go.
|
|
2
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"];
|
|
3
|
+
export const actorKinds = ["human","bot","external","task_run","scheduler_run","driver"];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdtemp, open, link, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Retain every numeric token verbatim, including values outside JS precision.
|
|
6
|
+
export const decodeRaw = (raw) => JSON.parse(raw, (_key, value, context) => typeof value === 'number' ? JSON.rawJSON(context.source) : value);
|
|
7
|
+
export const encodeRaw = (value) => JSON.stringify(value);
|
|
8
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && !JSON.isRawJSON(value);
|
|
9
|
+
const seq = (value) => {
|
|
10
|
+
if (!JSON.isRawJSON(value) || !/^-?\d+$/.test(value.rawJSON)) throw Error('invalid facts sequence');
|
|
11
|
+
const n = BigInt(value.rawJSON);
|
|
12
|
+
if (n < 0n || n > 9223372036854775807n) throw Error('invalid facts sequence');
|
|
13
|
+
return n;
|
|
14
|
+
};
|
|
15
|
+
const rawSeq = (n) => JSON.rawJSON(BigInt(n).toString());
|
|
16
|
+
|
|
17
|
+
export function validateReadOptions(o) {
|
|
18
|
+
const v = o.values;
|
|
19
|
+
if ('--fields' in v) {
|
|
20
|
+
if (!o.json) throw Error('--fields requires --json');
|
|
21
|
+
const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'creator', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
|
|
22
|
+
const paths = v['--fields'].split(',');
|
|
23
|
+
paths.forEach((field, i) => {
|
|
24
|
+
const parts = field.split('.');
|
|
25
|
+
if (!roots.includes(parts[0]) || (parts.length > 1 && !['metadata', 'payload'].includes(parts[0]))) throw Error(`unsupported field path ${JSON.stringify(field)} for ${o.command}`);
|
|
26
|
+
if (parts.some((part) => !part || part.trim() !== part || /[\[\]*\/\\\t\r\n]/.test(part))) throw Error(`invalid object field path ${JSON.stringify(field)}`);
|
|
27
|
+
if (paths.slice(0, i).some((earlier) => earlier === field || earlier.startsWith(field + '.') || field.startsWith(earlier + '.'))) throw Error('overlapping field paths');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const all = '--all' in v;
|
|
31
|
+
if (all && (!o.json || !v['--output-file']?.trim() || '--fields' in v)) throw Error('--all requires --json and --output-file, and cannot be combined with --fields');
|
|
32
|
+
for (const flag of ['--output-file', '--max-pages', '--max-bytes']) if (flag in v && !all) throw Error(`${flag} requires --all`);
|
|
33
|
+
for (const [flag, max] of [['--max-pages', 100], ['--max-bytes', 64 << 20]]) {
|
|
34
|
+
if (flag in v && (!/^\+?\d+$/.test(v[flag]) || BigInt(v[flag]) < 1n || BigInt(v[flag]) > BigInt(max))) throw Error(`${flag} must be between 1 and ${max}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectObject(source, paths) {
|
|
39
|
+
if (!isObject(source)) throw Error('expected a JSON object for field selection');
|
|
40
|
+
const selected = Object.create(null), children = new Map();
|
|
41
|
+
for (const field of paths) {
|
|
42
|
+
const [root, ...parts] = field.split('.');
|
|
43
|
+
if (parts.length) { if (!children.has(root)) children.set(root, []); children.get(root).push(parts.join('.')); }
|
|
44
|
+
else if (Object.hasOwn(source, root)) selected[root] = source[root];
|
|
45
|
+
}
|
|
46
|
+
for (const [root, subpaths] of children) {
|
|
47
|
+
if (!Object.hasOwn(source, root) || !isObject(source[root])) continue;
|
|
48
|
+
const projected = projectObject(source[root], subpaths);
|
|
49
|
+
if (Object.keys(projected).length) selected[root] = projected;
|
|
50
|
+
}
|
|
51
|
+
return selected;
|
|
52
|
+
}
|
|
53
|
+
export function projectResponse(command, raw, paths) {
|
|
54
|
+
const data = decodeRaw(raw);
|
|
55
|
+
if (command === 'get') return encodeRaw(projectObject(data, paths));
|
|
56
|
+
if (!isObject(data) || !Array.isArray(data.facts)) throw Error('expected facts array');
|
|
57
|
+
data.facts = data.facts.map((fact) => projectObject(fact, paths));
|
|
58
|
+
return encodeRaw(data);
|
|
59
|
+
}
|
|
60
|
+
const normalizedID = (id) => typeof id === 'string' ? id.toLowerCase().replace(/^urn:uuid:/, '').replace(/[{}-]/g, '') : '';
|
|
61
|
+
export function validateFactPage(raw, taskID, after) {
|
|
62
|
+
const page = decodeRaw(raw);
|
|
63
|
+
if (!isObject(page) || !Array.isArray(page.facts) || !Object.hasOwn(page, 'next_after_seq')) throw Error('invalid facts page identity, facts or cursor');
|
|
64
|
+
if (!/^[0-9a-f]{32}$/.test(normalizedID(page.task_id)) || normalizedID(page.task_id) !== normalizedID(taskID)) throw Error('facts page task_id does not match query');
|
|
65
|
+
let last = after;
|
|
66
|
+
for (const fact of page.facts) {
|
|
67
|
+
if (!isObject(fact) || typeof fact.type !== 'string' || !fact.type || seq(fact.seq) <= last) throw Error(`facts page is not strictly ordered after ${after}`);
|
|
68
|
+
last = seq(fact.seq);
|
|
69
|
+
}
|
|
70
|
+
if (seq(page.next_after_seq) !== last) throw Error(`facts cursor does not match last seq ${last}`);
|
|
71
|
+
return { facts: page.facts, next: last };
|
|
72
|
+
}
|
|
73
|
+
export async function readResponse(response, limit = 0) {
|
|
74
|
+
if (!limit) return response.text();
|
|
75
|
+
let bytes = 0; const chunks = [];
|
|
76
|
+
for await (const chunk of response.body) {
|
|
77
|
+
bytes += chunk.length;
|
|
78
|
+
if (bytes > limit) throw Object.assign(Error('read byte budget exhausted'), { byteLimit: true });
|
|
79
|
+
chunks.push(chunk);
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readAllFacts(o, baseURL, request, stdout, stderr) {
|
|
85
|
+
const output = path.resolve(o.values['--output-file']);
|
|
86
|
+
let temp;
|
|
87
|
+
try {
|
|
88
|
+
try { await lstat(output); throw Error(`output file must not exist: ${output}`); }
|
|
89
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
90
|
+
temp = await mkdtemp(path.join(path.dirname(output), '.task-facts-'));
|
|
91
|
+
} catch (error) { stderr.write(error.message + '\n'); return 2; }
|
|
92
|
+
try {
|
|
93
|
+
const query = { task_id: o.args[0], base_url: baseURL, after_seq: '0', limit: '200' };
|
|
94
|
+
for (const flag of ['--after', '--types', '--limit', '--metadata-source', '--metadata-query-key']) if (flag in o.values) query[flag === '--after' ? 'after_seq' : flag.slice(2).replaceAll('-', '_')] = o.values[flag];
|
|
95
|
+
const manifest = { query, complete: false, stop_reason: 'page_limit', pages: 0, count: 0, next_after_seq: rawSeq(query.after_seq), type_counts: Object.create(null), response_bytes: 0, output_file: output };
|
|
96
|
+
const facts = [], budget = Number(o.values['--max-bytes'] || (8 << 20));
|
|
97
|
+
let cursor = BigInt(query.after_seq);
|
|
98
|
+
for (let i = 0; i < Number(o.values['--max-pages'] || 10); i++) {
|
|
99
|
+
const remaining = budget - manifest.response_bytes;
|
|
100
|
+
if (remaining <= 0) { manifest.stop_reason = 'byte_limit'; break; }
|
|
101
|
+
let raw, page;
|
|
102
|
+
try { raw = await request({ ...o, values: { ...o.values, '--after': String(cursor) } }, remaining); }
|
|
103
|
+
catch (error) { manifest.stop_reason = error.byteLimit ? 'byte_limit' : 'request_error'; stderr.write(error.message + '\n'); break; }
|
|
104
|
+
manifest.response_bytes += Buffer.byteLength(raw);
|
|
105
|
+
try { page = validateFactPage(raw, o.args[0], cursor); }
|
|
106
|
+
catch (error) { manifest.stop_reason = 'invalid_page'; stderr.write(error.message + '\n'); break; }
|
|
107
|
+
manifest.pages++;
|
|
108
|
+
if (!page.facts.length) { manifest.complete = true; manifest.stop_reason = 'empty_page'; break; }
|
|
109
|
+
for (const fact of page.facts) {
|
|
110
|
+
if (!manifest.count) manifest.first_seq = fact.seq;
|
|
111
|
+
manifest.last_seq = fact.seq;
|
|
112
|
+
manifest.type_counts[fact.type] = (manifest.type_counts[fact.type] || 0) + 1;
|
|
113
|
+
manifest.count++;
|
|
114
|
+
}
|
|
115
|
+
facts.push(...page.facts); cursor = page.next; manifest.next_after_seq = rawSeq(cursor);
|
|
116
|
+
}
|
|
117
|
+
const data = Buffer.from(encodeRaw({ query, facts, complete: manifest.complete, stop_reason: manifest.stop_reason, next_after_seq: manifest.next_after_seq }));
|
|
118
|
+
const staging = path.join(temp, 'facts.json'), file = await open(staging, 'wx', 0o600);
|
|
119
|
+
try { await file.writeFile(data); } finally { await file.close(); }
|
|
120
|
+
await link(staging, output);
|
|
121
|
+
manifest.file_bytes = data.length; manifest.file_sha256 = createHash('sha256').update(data).digest('hex');
|
|
122
|
+
stdout.write(encodeRaw(manifest) + '\n');
|
|
123
|
+
if (!manifest.complete) { stderr.write(`incomplete facts read: ${manifest.stop_reason}; next_after_seq=${cursor}\n`); return 1; }
|
|
124
|
+
return 0;
|
|
125
|
+
} catch (error) { stderr.write(error.message + '\n'); return 1; }
|
|
126
|
+
finally { await rm(temp, { recursive: true, force: true }); }
|
|
127
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.2","project":"deepflow-task","sourceCommit":"cc53e86834b19e8a4fe73f9e9a08d76853d51e51","sourceDirty":false}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.2","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
|