@fieldwangai/agentflow 0.1.166 → 0.1.167
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/bin/lib/agent-runners.mjs +27 -9
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/composer-agent.mjs +11 -0
- package/bin/lib/cursor-api-key-pool.mjs +106 -1
- package/bin/lib/repository-index-events.mjs +17 -0
- package/bin/lib/repository-index.mjs +522 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +543 -210
- package/bin/lib/workspace-server.mjs +2 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DKdBGu3q.js → WorkflowAssistantThread-B0i4F0Ab.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BLTi7FF5.js +877 -0
- package/builtin/web-ui/dist/assets/index-yplDmRpj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-ai-exploration/SKILL.md +127 -0
- package/skills/agentflow-ai-exploration/agents/openai.yaml +4 -0
- package/skills/agentflow-ai-exploration/references/protocol.md +120 -0
- package/skills/agentflow-ai-exploration/scripts/agentflow-ai-exploration.mjs +308 -0
- package/skills/agentflow-ai-exploration/scripts/auth-store.mjs +102 -0
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +1 -1
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B7YuvFR2.css +0 -1
- package/builtin/web-ui/dist/assets/index-BUljbvrW.js +0 -877
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
@keyframes af-app-loading-slide { from { transform: translateX(-115%); } to { transform: translateX(250%); } }
|
|
31
31
|
@media (prefers-reduced-motion: reduce) { .af-app-loading__track span { width: 100%; animation: none; } }
|
|
32
32
|
</style>
|
|
33
|
-
<script type="module" crossorigin src="/assets/index-
|
|
34
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
33
|
+
<script type="module" crossorigin src="/assets/index-BLTi7FF5.js"></script>
|
|
34
|
+
<link rel="stylesheet" crossorigin href="/assets/index-yplDmRpj.css">
|
|
35
35
|
</head>
|
|
36
36
|
<body>
|
|
37
37
|
<div id="root">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldwangai/agentflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.167",
|
|
4
4
|
"description": "Orchestration system for long-running complex agent tasks using Cursor, OpenCode, Claude Code, or Codex as execution backends",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/agentflow.mjs",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentflow-ai-exploration
|
|
3
|
+
description: Plan, visualize, audit, and archive AI execution through AgentFlow without MCP. Use when Codex or another agent needs to create a read-only expected execution graph, stream planned or actual Trace/Span events into a Workspace, perform a side-effect-safe dry-run policy check, inspect an external agent run, or materialize an approved exploration as editable workspace.flow.js DSL without running or publishing it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AgentFlow AI Exploration
|
|
7
|
+
|
|
8
|
+
Use the bundled browser-authorized CLI to turn an AI plan or live agent run into an auditable AgentFlow Trace. Resolve `<skill-dir>` as the directory containing this file:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs <command> [options]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Do not configure MCP or install npm dependencies.
|
|
15
|
+
|
|
16
|
+
## Authorize
|
|
17
|
+
|
|
18
|
+
Check configuration first:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs config
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If `hasToken` is false, start browser authorization and return the `verificationUrl` to the user. Never ask them to paste a Token:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs auth start
|
|
28
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs auth complete
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The CLI shares the owner-only `~/.agentflow/auth.json` profile with `agentflow-cli`. Never print credentials.
|
|
32
|
+
|
|
33
|
+
## Choose the workflow
|
|
34
|
+
|
|
35
|
+
- For “先给我计划 / dry-run / 提前看看怎么执行”, generate a server Plan, inspect it, optionally run the policy check, and stop. Do not execute the task.
|
|
36
|
+
- For “把这次 Codex/Agent 执行可视化”, create an observed Session before work, append meaningful events as actions occur, and finish the Session at the end.
|
|
37
|
+
- For “把探索结果变成流程”, inspect the Trace, review side effects, then materialize it into the current Workspace adjustment state. Do not publish, run, or enable schedules.
|
|
38
|
+
- For read-only review, use `list` and `get`; do not create or append anything.
|
|
39
|
+
|
|
40
|
+
## Generate an expected graph
|
|
41
|
+
|
|
42
|
+
Call the read-only Plan endpoint:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs plan \
|
|
46
|
+
--goal "分析失败任务并生成修复步骤" \
|
|
47
|
+
--flow-id <flow-id>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Read every planned event before reporting. Distinguish `none/read` steps from `write/external` steps and call out `requiresApproval` entries. Plan means expected behavior, never completed behavior.
|
|
51
|
+
|
|
52
|
+
To check policy without running tools:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs dry-run \
|
|
56
|
+
--id <session-id> \
|
|
57
|
+
--flow-id <flow-id>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This dry-run only classifies and blocks side effects. Always say `executedTools: false`; never describe it as a sandbox execution.
|
|
61
|
+
|
|
62
|
+
## Trace an external agent run
|
|
63
|
+
|
|
64
|
+
Create one Session before acting:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs create \
|
|
68
|
+
--title "Codex 修复探索" \
|
|
69
|
+
--goal "定位失败并形成可复用流程" \
|
|
70
|
+
--flow-id <flow-id>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Keep the returned `exploration.id`. Append one event per meaningful decision, tool call, file change, command, or artifact. Prefer `--file` for reliable JSON:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs append \
|
|
77
|
+
--id <session-id> \
|
|
78
|
+
--flow-id <flow-id> \
|
|
79
|
+
--phase observed \
|
|
80
|
+
--file /absolute/path/to/events.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use stable `spanId` and `parentSpanId` values. Mark events `running` until their outcome is known, then append the completion/error event. Do not claim success at tool start. Classify side effects conservatively:
|
|
84
|
+
|
|
85
|
+
- `none`: reasoning or local decision only
|
|
86
|
+
- `read`: local search, inspection, or read-only query
|
|
87
|
+
- `write`: files, git state, configuration, or local mutation
|
|
88
|
+
- `external`: HTTP writes, messages, publishing, deployment, or remote mutation
|
|
89
|
+
|
|
90
|
+
Do not emit hidden reasoning, every streamed token, complete secret-bearing inputs, or noisy low-level status. The server redacts common secrets, but omit them before upload.
|
|
91
|
+
|
|
92
|
+
Always close the Session, including failure paths:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs finish \
|
|
96
|
+
--id <session-id> \
|
|
97
|
+
--flow-id <flow-id> \
|
|
98
|
+
--status completed \
|
|
99
|
+
--summary "完成定位并生成修复建议"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `--status failed` with a concise error summary when execution fails.
|
|
103
|
+
|
|
104
|
+
## Materialize reviewed Trace
|
|
105
|
+
|
|
106
|
+
First read the complete Session:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs get --id <session-id> --flow-id <flow-id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
If materializable events include `write`, `external`, or `requiresApproval: true`, show their names and effects to the user. Pass `--approve-side-effects` only after the user explicitly approves those listed effects:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
node <skill-dir>/scripts/agentflow-ai-exploration.mjs materialize \
|
|
116
|
+
--id <session-id> \
|
|
117
|
+
--flow-id <flow-id> \
|
|
118
|
+
--approve-side-effects
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Materialization edits the Workspace DSL adjustment state and records provenance. It does not execute, publish, replace a stable release, or activate Scheduled Run. Use `agentflow-author-flow` afterward when the user asks to test and publish the generated graph.
|
|
122
|
+
|
|
123
|
+
## Target the right Workspace
|
|
124
|
+
|
|
125
|
+
Omit `--flow-id` only when operating the server's current Workspace root. For stored flows, pass the exact `--flow-id` and `--flow-source`. Do not guess an owner or target. Admin read scope may use `--admin-owner-id`, but write endpoints still enforce Workspace permissions.
|
|
126
|
+
|
|
127
|
+
Read [references/protocol.md](references/protocol.md) when constructing custom events, integrating another Agent SDK, or diagnosing API/CLI errors.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AI Exploration protocol
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
|
|
5
|
+
All commands return JSON:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
config
|
|
9
|
+
auth start | complete | status | logout
|
|
10
|
+
list [workspace scope]
|
|
11
|
+
get --id <session-id> [workspace scope]
|
|
12
|
+
create [--title] [--goal] [--mode observed|planned] [workspace scope]
|
|
13
|
+
plan --goal <text> [--model <key>] [workspace scope]
|
|
14
|
+
append --id <session-id> (--file <json> | --event <json> | --stdin) [--phase <phase>] [workspace scope]
|
|
15
|
+
finish --id <session-id> [--status completed|failed] [--summary <text>] [workspace scope]
|
|
16
|
+
dry-run --id <session-id> [workspace scope]
|
|
17
|
+
materialize --id <session-id> [--model <key>] [--approve-side-effects] [workspace scope]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Workspace scope options are `--flow-id`, `--flow-source`, `--admin-owner-id`, and `--archived`.
|
|
21
|
+
|
|
22
|
+
Configuration order:
|
|
23
|
+
|
|
24
|
+
1. `--base-url`, `--token`
|
|
25
|
+
2. `AGENTFLOW_BASE_URL`, `AGENTFLOW_TOKEN`, `AGENTFLOW_SESSION_TOKEN`
|
|
26
|
+
3. `AGENTFLOW_ENV_FILE`, `.env`, `.agentflow.env`, `~/.agentflow.env`
|
|
27
|
+
4. `~/.agentflow/auth.json`
|
|
28
|
+
|
|
29
|
+
Default server: `http://ai.mengma.bigo.inner/`.
|
|
30
|
+
|
|
31
|
+
## Trace event
|
|
32
|
+
|
|
33
|
+
An append file may be one event, an event array, or `{ "events": [...] }`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"id": "read_log_complete",
|
|
38
|
+
"traceId": "",
|
|
39
|
+
"spanId": "read_log",
|
|
40
|
+
"parentSpanId": "turn_1",
|
|
41
|
+
"type": "tool",
|
|
42
|
+
"name": "Read failure log",
|
|
43
|
+
"summary": "Found a timeout in the delivery request",
|
|
44
|
+
"status": "success",
|
|
45
|
+
"sideEffect": "read",
|
|
46
|
+
"requiresApproval": false,
|
|
47
|
+
"startedAt": "2026-08-25T10:00:00.000Z",
|
|
48
|
+
"endedAt": "2026-08-25T10:00:01.000Z",
|
|
49
|
+
"inputPreview": "run-id: workspace_123",
|
|
50
|
+
"outputPreview": "request timeout after 60s",
|
|
51
|
+
"artifacts": [
|
|
52
|
+
{ "kind": "log", "path": "logs/failure.txt", "label": "Failure log" }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Allowed values:
|
|
58
|
+
|
|
59
|
+
- `phase`: `planned`, `simulated`, `observed`, `materialized`
|
|
60
|
+
- `type`: `run`, `turn`, `decision`, `agent`, `tool`, `command`, `file`, `artifact`, `status`
|
|
61
|
+
- `status`: `planned`, `running`, `success`, `error`, `blocked`, `skipped`
|
|
62
|
+
- `sideEffect`: `none`, `read`, `write`, `external`
|
|
63
|
+
|
|
64
|
+
The server assigns `traceId`, sequence, and timestamps when omitted. It limits a Session to 5000 events and truncates previews. `write` and `external` automatically imply approval.
|
|
65
|
+
|
|
66
|
+
For a long operation, reuse the same `spanId` across start and terminal events while giving each event a unique `id`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
"id": "command_1_start",
|
|
72
|
+
"spanId": "command_1",
|
|
73
|
+
"type": "command",
|
|
74
|
+
"name": "Run tests",
|
|
75
|
+
"status": "running",
|
|
76
|
+
"sideEffect": "read"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "command_1_finish",
|
|
80
|
+
"spanId": "command_1",
|
|
81
|
+
"type": "command",
|
|
82
|
+
"name": "Run tests",
|
|
83
|
+
"summary": "256 tests passed",
|
|
84
|
+
"status": "success",
|
|
85
|
+
"sideEffect": "read"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## HTTP mapping
|
|
91
|
+
|
|
92
|
+
The bundled CLI maps to these authenticated endpoints:
|
|
93
|
+
|
|
94
|
+
| Operation | Endpoint |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| list | `GET /api/workspace/explorations` |
|
|
97
|
+
| get | `GET /api/workspace/exploration` |
|
|
98
|
+
| create | `POST /api/workspace/exploration` |
|
|
99
|
+
| append / finish | `POST /api/workspace/exploration/events` |
|
|
100
|
+
| plan | `POST /api/workspace/exploration/plan` |
|
|
101
|
+
| dry-run | `POST /api/workspace/exploration/dry-run` |
|
|
102
|
+
| materialize | `POST /api/workspace/exploration/materialize` |
|
|
103
|
+
|
|
104
|
+
Use the CLI unless integrating a runtime that cannot launch Node. Direct HTTP clients must send the same Bearer Token and Workspace scope fields.
|
|
105
|
+
|
|
106
|
+
## Semantics
|
|
107
|
+
|
|
108
|
+
- Plan uses a read-only agent configuration where the selected backend supports it and returns expected spans only.
|
|
109
|
+
- Dry-run is a deterministic side-effect policy check. It does not call tools.
|
|
110
|
+
- Observed Trace records what actually happened; it must not rewrite planned events.
|
|
111
|
+
- Materialization prefers planned spans when present, otherwise observed spans. Simulated events are never converted into DSL nodes.
|
|
112
|
+
- Materialization writes the editable adjustment state only. Stable release, execution, publication, and schedule activation remain separate operations.
|
|
113
|
+
|
|
114
|
+
## Error handling
|
|
115
|
+
|
|
116
|
+
- `401/403`: reauthorize or verify Workspace ownership; never retry with a copied credential.
|
|
117
|
+
- `404`: verify Session ID and the exact Workspace scope used to create it.
|
|
118
|
+
- `409` from materialize: inspect the returned side effects, ask for explicit approval, then rerun with `--approve-side-effects` only if approved.
|
|
119
|
+
- Failed Plan: inspect the returned `explorationId`; the failed Session remains available for audit.
|
|
120
|
+
- Failed materialization: the Session is marked failed and retains the observed error event. Do not publish the partial DSL.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
authFile,
|
|
7
|
+
clearPending,
|
|
8
|
+
clearProfile,
|
|
9
|
+
savePending,
|
|
10
|
+
saveProfile,
|
|
11
|
+
savedPending,
|
|
12
|
+
savedProfile,
|
|
13
|
+
} from "./auth-store.mjs";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_BASE_URL = "http://ai.mengma.bigo.inner/";
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
return `AgentFlow AI Exploration CLI
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
agentflow-ai-exploration <command> [options]
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
config
|
|
25
|
+
auth start | complete | status | logout
|
|
26
|
+
list [--flow-id <id>] [--flow-source user]
|
|
27
|
+
get --id <session-id> [--flow-id <id>]
|
|
28
|
+
create [--title <text>] [--goal <text>] [--mode observed|planned]
|
|
29
|
+
plan --goal <text> [--model <key>]
|
|
30
|
+
append --id <session-id> (--file <events.json> | --event <json> | --stdin) [--phase observed|planned]
|
|
31
|
+
finish --id <session-id> [--status completed|failed] [--summary <text>]
|
|
32
|
+
dry-run --id <session-id>
|
|
33
|
+
materialize --id <session-id> [--model <key>] [--approve-side-effects]
|
|
34
|
+
|
|
35
|
+
Shared options:
|
|
36
|
+
--flow-id <id> Target a stored Flow Workspace instead of the current Workspace
|
|
37
|
+
--flow-source <source> user|workspace (default: user)
|
|
38
|
+
--admin-owner-id <id> Admin review scope
|
|
39
|
+
--archived Target an archived Flow
|
|
40
|
+
--base-url <url> Override AGENTFLOW_BASE_URL
|
|
41
|
+
--token <token> Override saved browser authorization
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgv(argv) {
|
|
46
|
+
const output = { _: [] };
|
|
47
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
48
|
+
const item = argv[index];
|
|
49
|
+
if (!item.startsWith("--")) {
|
|
50
|
+
output._.push(item);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const equal = item.indexOf("=");
|
|
54
|
+
let name = item.slice(2);
|
|
55
|
+
let value = true;
|
|
56
|
+
if (equal >= 0) {
|
|
57
|
+
name = item.slice(2, equal);
|
|
58
|
+
value = item.slice(equal + 1);
|
|
59
|
+
} else if (argv[index + 1] && !argv[index + 1].startsWith("--")) {
|
|
60
|
+
value = argv[index + 1];
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
output[name] = value;
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function option(args, name, fallback = "") {
|
|
69
|
+
const value = args[name];
|
|
70
|
+
return value === undefined || value === null || value === true ? fallback : String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadDotenvFile(file) {
|
|
74
|
+
if (!file || !fs.existsSync(file)) return;
|
|
75
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
76
|
+
const match = line.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
77
|
+
if (!match || process.env[match[1]] !== undefined) continue;
|
|
78
|
+
let value = match[2].trim();
|
|
79
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
80
|
+
process.env[match[1]] = value.replace(/\\n/g, "\n");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadEnvironment() {
|
|
85
|
+
for (const candidate of [
|
|
86
|
+
process.env.AGENTFLOW_ENV_FILE,
|
|
87
|
+
path.join(process.cwd(), ".env"),
|
|
88
|
+
path.join(process.cwd(), ".agentflow.env"),
|
|
89
|
+
path.join(os.homedir(), ".agentflow.env"),
|
|
90
|
+
].filter(Boolean)) loadDotenvFile(path.resolve(candidate));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function baseUrl(args) {
|
|
94
|
+
return String(option(args, "base-url") || process.env.AGENTFLOW_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolvedAuth(args) {
|
|
98
|
+
const direct = option(args, "token");
|
|
99
|
+
if (direct) return { token: direct, source: "flag", profile: null };
|
|
100
|
+
if (String(process.env.AGENTFLOW_TOKEN || "").trim()) return { token: String(process.env.AGENTFLOW_TOKEN).trim(), source: "AGENTFLOW_TOKEN", profile: null };
|
|
101
|
+
if (String(process.env.AGENTFLOW_SESSION_TOKEN || "").trim()) return { token: String(process.env.AGENTFLOW_SESSION_TOKEN).trim(), source: "AGENTFLOW_SESSION_TOKEN", profile: null };
|
|
102
|
+
const profile = savedProfile(baseUrl(args));
|
|
103
|
+
return profile?.token ? { token: profile.token, source: "saved-auth", profile } : { token: "", source: "", profile: null };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function token(args, required = true) {
|
|
107
|
+
const value = String(resolvedAuth(args).token || "").trim();
|
|
108
|
+
if (required && !value) throw new Error("AgentFlow authorization is missing. Run `auth start`, approve the returned URL, then run `auth complete`.");
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function httpJson(args, pathname, { method = "GET", body, tokenRequired = true } = {}) {
|
|
113
|
+
const authToken = token(args, tokenRequired);
|
|
114
|
+
const url = new URL(pathname, `${baseUrl(args)}/`);
|
|
115
|
+
const headers = { Accept: "application/json" };
|
|
116
|
+
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
117
|
+
if (authToken) {
|
|
118
|
+
headers.Authorization = `Bearer ${authToken}`;
|
|
119
|
+
headers.Cookie = `af_session=${encodeURIComponent(authToken)}`;
|
|
120
|
+
}
|
|
121
|
+
const response = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
122
|
+
const text = await response.text();
|
|
123
|
+
let data;
|
|
124
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = { text }; }
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const error = new Error(data?.error || data?.message || text || `HTTP ${response.status}`);
|
|
127
|
+
error.status = response.status;
|
|
128
|
+
error.response = data;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scope(args) {
|
|
135
|
+
return {
|
|
136
|
+
flowId: option(args, "flow-id"),
|
|
137
|
+
flowSource: option(args, "flow-source", "user") || "user",
|
|
138
|
+
...(option(args, "admin-owner-id") ? { adminOwnerId: option(args, "admin-owner-id") } : {}),
|
|
139
|
+
...(args.archived === true ? { archived: true } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function query(input) {
|
|
144
|
+
const params = new URLSearchParams();
|
|
145
|
+
for (const [name, value] of Object.entries(input)) {
|
|
146
|
+
if (value === "" || value === undefined || value === null || value === false) continue;
|
|
147
|
+
params.set(name, value === true ? "1" : String(value));
|
|
148
|
+
}
|
|
149
|
+
const output = params.toString();
|
|
150
|
+
return output ? `?${output}` : "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function required(args, name) {
|
|
154
|
+
const value = option(args, name);
|
|
155
|
+
if (!value) throw new Error(`Missing --${name}.`);
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readJsonInput(args) {
|
|
160
|
+
let source = "";
|
|
161
|
+
if (option(args, "file")) source = fs.readFileSync(path.resolve(option(args, "file")), "utf8");
|
|
162
|
+
else if (option(args, "event")) source = option(args, "event");
|
|
163
|
+
else if (args.stdin === true) source = fs.readFileSync(0, "utf8");
|
|
164
|
+
else throw new Error("append requires --file <events.json>, --event <json>, or --stdin.");
|
|
165
|
+
let parsed;
|
|
166
|
+
try { parsed = JSON.parse(source); } catch (error) { throw new Error(`Invalid Trace JSON: ${error.message}`); }
|
|
167
|
+
if (Array.isArray(parsed)) return parsed;
|
|
168
|
+
if (Array.isArray(parsed?.events)) return parsed.events;
|
|
169
|
+
if (parsed && typeof parsed === "object") return [parsed];
|
|
170
|
+
throw new Error("Trace JSON must be an event object, an event array, or an object with events[].");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function print(value) {
|
|
174
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function runAuth(action, args) {
|
|
178
|
+
const target = baseUrl(args);
|
|
179
|
+
if (action === "start" || action === "login") {
|
|
180
|
+
const result = await httpJson(args, "/api/auth/cli/device", {
|
|
181
|
+
method: "POST",
|
|
182
|
+
tokenRequired: false,
|
|
183
|
+
body: { clientName: "AgentFlow AI Exploration" },
|
|
184
|
+
});
|
|
185
|
+
const pending = savePending(target, result);
|
|
186
|
+
print({ status: "authorization_required", baseUrl: target, requestId: pending.requestId, userCode: pending.userCode, verificationUrl: pending.verificationUrl, expiresAt: pending.expiresAt });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (action === "complete") {
|
|
190
|
+
const pending = savedPending(target);
|
|
191
|
+
if (!pending?.deviceCode) throw new Error("No pending authorization. Run `auth start` first.");
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = await httpJson(args, "/api/auth/cli/token", { method: "POST", tokenRequired: false, body: { deviceCode: pending.deviceCode } });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (["access_denied", "expired_token", "invalid_grant"].includes(String(error?.response?.code || ""))) clearPending(target);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
if (result?.code === "authorization_pending") {
|
|
200
|
+
print({ status: "authorization_pending", baseUrl: target, verificationUrl: pending.verificationUrl, expiresAt: pending.expiresAt });
|
|
201
|
+
process.exitCode = 2;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!result?.token) throw new Error("Authorization exchange did not return a token.");
|
|
205
|
+
const saved = saveProfile(target, result);
|
|
206
|
+
print({ status: "authenticated", baseUrl: target, user: result.user || null, scopes: result.scopes || [], expiresAt: result.expiresAt || 0, credentialFile: saved.file });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (action === "status") {
|
|
210
|
+
const auth = resolvedAuth(args);
|
|
211
|
+
if (!auth.token) {
|
|
212
|
+
print({ authenticated: false, baseUrl: target, credentialFile: authFile() });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const me = await httpJson(args, "/api/auth/me", { tokenRequired: false });
|
|
216
|
+
print({ authenticated: Boolean(me?.authenticated), baseUrl: target, tokenSource: auth.source, user: me?.user || null, credentialFile: auth.source === "saved-auth" ? authFile() : "" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (action === "logout") {
|
|
220
|
+
const auth = resolvedAuth(args);
|
|
221
|
+
let revoked = false;
|
|
222
|
+
if (auth.token) {
|
|
223
|
+
try { revoked = Boolean((await httpJson(args, "/api/auth/cli/revoke", { method: "POST", body: {} }))?.ok); } catch {}
|
|
224
|
+
}
|
|
225
|
+
print({ authenticated: false, baseUrl: target, revoked, localCleared: clearProfile(target), tokenSource: auth.source });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new Error("Unknown auth action. Use start, complete, status, or logout.");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function main() {
|
|
232
|
+
loadEnvironment();
|
|
233
|
+
const args = parseArgv(process.argv.slice(2));
|
|
234
|
+
const command = String(args._[0] || "help").toLowerCase();
|
|
235
|
+
if (["help", "--help", "-h"].includes(command)) {
|
|
236
|
+
process.stdout.write(usage());
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (command === "auth") {
|
|
240
|
+
await runAuth(String(args._[1] || "status").toLowerCase(), args);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (command === "config") {
|
|
244
|
+
const auth = resolvedAuth(args);
|
|
245
|
+
print({ baseUrl: baseUrl(args), hasToken: Boolean(auth.token), tokenSource: auth.source, credentialFile: auth.source === "saved-auth" ? authFile() : "" });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (command === "list") {
|
|
249
|
+
print(await httpJson(args, `/api/workspace/explorations${query(scope(args))}`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (command === "get") {
|
|
253
|
+
print(await httpJson(args, `/api/workspace/exploration${query({ ...scope(args), id: required(args, "id") })}`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (command === "create") {
|
|
257
|
+
print(await httpJson(args, "/api/workspace/exploration", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: {
|
|
260
|
+
...scope(args),
|
|
261
|
+
title: option(args, "title") || option(args, "goal") || "External Agent run",
|
|
262
|
+
goal: option(args, "goal"),
|
|
263
|
+
mode: option(args, "mode", "observed") || "observed",
|
|
264
|
+
status: option(args, "status", "running") || "running",
|
|
265
|
+
source: { provider: option(args, "provider", "external") || "external", agent: option(args, "agent", "Codex / Agent SDK") || "Codex / Agent SDK" },
|
|
266
|
+
},
|
|
267
|
+
}));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (command === "plan") {
|
|
271
|
+
print(await httpJson(args, "/api/workspace/exploration/plan", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: { ...scope(args), goal: required(args, "goal"), model: option(args, "model") },
|
|
274
|
+
}));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (command === "append") {
|
|
278
|
+
print(await httpJson(args, "/api/workspace/exploration/events", {
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: { ...scope(args), id: required(args, "id"), phase: option(args, "phase", "observed") || "observed", events: readJsonInput(args) },
|
|
281
|
+
}));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (command === "finish") {
|
|
285
|
+
print(await httpJson(args, "/api/workspace/exploration/events", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: { ...scope(args), id: required(args, "id"), events: [], status: option(args, "status", "completed") || "completed", summary: option(args, "summary") },
|
|
288
|
+
}));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (command === "dry-run") {
|
|
292
|
+
print(await httpJson(args, "/api/workspace/exploration/dry-run", { method: "POST", body: { ...scope(args), id: required(args, "id") } }));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (command === "materialize") {
|
|
296
|
+
print(await httpJson(args, "/api/workspace/exploration/materialize", {
|
|
297
|
+
method: "POST",
|
|
298
|
+
body: { ...scope(args), id: required(args, "id"), model: option(args, "model"), approveSideEffects: args["approve-side-effects"] === true },
|
|
299
|
+
}));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`Unknown command: ${command}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
main().catch((error) => {
|
|
306
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error?.message || String(error), status: error?.status || 0, response: error?.response || null }, null, 2)}\n`);
|
|
307
|
+
process.exitCode = 1;
|
|
308
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function emptyStore() {
|
|
7
|
+
return { version: 1, profiles: {}, pending: {} };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function authFile() {
|
|
11
|
+
const configured = String(process.env.AGENTFLOW_AUTH_FILE || "").trim();
|
|
12
|
+
return path.resolve(configured || path.join(os.homedir(), ".agentflow", "auth.json"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readStore() {
|
|
16
|
+
try {
|
|
17
|
+
const data = JSON.parse(fs.readFileSync(authFile(), "utf8"));
|
|
18
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return emptyStore();
|
|
19
|
+
return {
|
|
20
|
+
version: 1,
|
|
21
|
+
profiles: data.profiles && typeof data.profiles === "object" && !Array.isArray(data.profiles) ? data.profiles : {},
|
|
22
|
+
pending: data.pending && typeof data.pending === "object" && !Array.isArray(data.pending) ? data.pending : {},
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return emptyStore();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeStore(store) {
|
|
30
|
+
const file = authFile();
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
32
|
+
const temporary = `${file}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
33
|
+
fs.writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
34
|
+
fs.renameSync(temporary, file);
|
|
35
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
36
|
+
return file;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function key(baseUrl) {
|
|
40
|
+
return String(baseUrl || "").replace(/\/+$/, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function savedProfile(baseUrl) {
|
|
44
|
+
const profile = readStore().profiles[key(baseUrl)];
|
|
45
|
+
if (!profile || typeof profile !== "object") return null;
|
|
46
|
+
if (Number(profile.expiresAt) > 0 && Number(profile.expiresAt) <= Date.now()) return null;
|
|
47
|
+
return String(profile.token || "").trim() ? profile : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function savedPending(baseUrl) {
|
|
51
|
+
const pending = readStore().pending[key(baseUrl)];
|
|
52
|
+
if (!pending || typeof pending !== "object") return null;
|
|
53
|
+
if (Number(pending.expiresAt) > 0 && Number(pending.expiresAt) <= Date.now()) return null;
|
|
54
|
+
return pending;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function savePending(baseUrl, input) {
|
|
58
|
+
const store = readStore();
|
|
59
|
+
store.pending[key(baseUrl)] = {
|
|
60
|
+
requestId: String(input.requestId || ""),
|
|
61
|
+
deviceCode: String(input.deviceCode || ""),
|
|
62
|
+
userCode: String(input.userCode || ""),
|
|
63
|
+
verificationUrl: String(input.verificationUrl || ""),
|
|
64
|
+
expiresAt: Number(input.expiresAt) || 0,
|
|
65
|
+
pollInterval: Number(input.pollInterval) || 3,
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
writeStore(store);
|
|
69
|
+
return { ...store.pending[key(baseUrl)], deviceCode: undefined };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function saveProfile(baseUrl, input) {
|
|
73
|
+
const store = readStore();
|
|
74
|
+
store.profiles[key(baseUrl)] = {
|
|
75
|
+
token: String(input.token || ""),
|
|
76
|
+
tokenType: String(input.tokenType || "Bearer"),
|
|
77
|
+
expiresAt: Number(input.expiresAt) || 0,
|
|
78
|
+
user: input.user && typeof input.user === "object" ? input.user : null,
|
|
79
|
+
scopes: Array.isArray(input.scopes) ? input.scopes.map(String) : [],
|
|
80
|
+
updatedAt: Date.now(),
|
|
81
|
+
};
|
|
82
|
+
delete store.pending[key(baseUrl)];
|
|
83
|
+
const file = writeStore(store);
|
|
84
|
+
return { file, profile: { ...store.profiles[key(baseUrl)], token: undefined } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function clearPending(baseUrl) {
|
|
88
|
+
const store = readStore();
|
|
89
|
+
if (!store.pending[key(baseUrl)]) return false;
|
|
90
|
+
delete store.pending[key(baseUrl)];
|
|
91
|
+
writeStore(store);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function clearProfile(baseUrl) {
|
|
96
|
+
const store = readStore();
|
|
97
|
+
const existed = Boolean(store.profiles[key(baseUrl)] || store.pending[key(baseUrl)]);
|
|
98
|
+
delete store.profiles[key(baseUrl)];
|
|
99
|
+
delete store.pending[key(baseUrl)];
|
|
100
|
+
if (existed) writeStore(store);
|
|
101
|
+
return existed;
|
|
102
|
+
}
|