factory-ai 1.6.0 → 2.0.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.
- package/CHANGELOG.md +15 -2
- package/README.md +6 -0
- package/bin/factory +44 -3
- package/cmd/factory-ui/main.go +242 -25
- package/package.json +1 -3
- package/src/approval-cli.js +17 -0
- package/src/tui.js +0 -173
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,24 @@ All notable changes follow semantic versioning and the Keep a Changelog structur
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.0] - 2026-07-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Replaced the JavaScript fallback TUI with one native Go operator console featuring an OpenCode-style command bar, command history, inline output/errors, and shortcuts for submissions, workspace imports, secrets, approvals, pause/resume, and help.
|
|
12
|
+
- Removed the `factory-ui` JavaScript binary and `neo-blessed` dependency; `factory ui` now fails closed if its verified attested native binary is unavailable.
|
|
13
|
+
|
|
14
|
+
## [1.6.1] - 2026-07-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `factory update check|now|status|enable|disable` for local version checks, immediate verified Azure/local updates, and explicit six-hour timer control.
|
|
19
|
+
|
|
7
20
|
## [1.6.0] - 2026-07-13
|
|
8
21
|
|
|
9
22
|
### Added
|
|
10
23
|
|
|
11
|
-
- Persistent Conductor-style workspace catalog with local/GitHub imports, managed clones, stable names, default branches, project initialization, CLI commands, direct Service Bus submission, and
|
|
24
|
+
- Persistent Conductor-style workspace catalog with local/GitHub imports, managed clones, stable names, default branches, project initialization, CLI commands, direct Service Bus submission, and a native workspace view.
|
|
12
25
|
|
|
13
26
|
## [1.5.3] - 2026-07-13
|
|
14
27
|
|
|
@@ -32,7 +45,7 @@ All notable changes follow semantic versioning and the Keep a Changelog structur
|
|
|
32
45
|
|
|
33
46
|
### Added
|
|
34
47
|
|
|
35
|
-
- Cross-platform Bubble Tea/Lip Gloss operator client with direct Azure Blob snapshots, verified release binaries, scrolling, and
|
|
48
|
+
- Cross-platform Bubble Tea/Lip Gloss operator client with direct Azure Blob snapshots, verified release binaries, scrolling, and native command execution.
|
|
36
49
|
- Automatic Azure and Bedrock context compaction with immutable objective retention and recent tool evidence.
|
|
37
50
|
- Hierarchical `AGENTS.md` discovery, preconfigured project instructions, and bounded `.agent-factory` context.
|
|
38
51
|
- Durable worktree crash recovery, parallel read-only tool batches, live activity timelines, retry/error telemetry, and stale-agent watchdog recovery.
|
package/README.md
CHANGED
|
@@ -74,6 +74,10 @@ factory ui
|
|
|
74
74
|
| `factory workspace show NAME` | Inspect one workspace |
|
|
75
75
|
| `factory workspace remove NAME` | Remove a catalog entry without deleting repository files |
|
|
76
76
|
| `factory submit WORKSPACE "OBJECTIVE"` | Send one CEO objective using a workspace name; owner/repo remains supported |
|
|
77
|
+
| `factory update check` | Compare the installed CLI with the latest stable npm release |
|
|
78
|
+
| `factory update now` | Run verified Azure updater immediately and update the local CLI |
|
|
79
|
+
| `factory update status` | Show automatic-update timer and deployed runtime version |
|
|
80
|
+
| `factory update enable\|disable` | Control the six-hour verified automatic-update timer |
|
|
77
81
|
| `factory issue OWNER/REPO NUMBER` | Turn a GitHub issue into a tracked objective |
|
|
78
82
|
| `factory telegram configure` | Configure allowlisted Telegram remote intake |
|
|
79
83
|
| `factory dashboard` | Objectives, agents, models, queue, DLQ, and Azure cost |
|
|
@@ -92,6 +96,8 @@ factory ui
|
|
|
92
96
|
| `factory secret set NAME` | Store a credential in global Key Vault |
|
|
93
97
|
| `factory github connect ORG` | Connect GitHub Enterprise credentials |
|
|
94
98
|
|
|
99
|
+
Inside `factory ui`, press `:` to enter any Factory command without the `factory` prefix. Use `↑/↓` for command history. Shortcuts: `n` submit, `i` import workspace, `a` set secret, `y/x` approve or deny, `p/u` pause or resume, `?` help, and `q` quit. Command output and errors remain visible inside the native TUI.
|
|
100
|
+
|
|
95
101
|
## Architecture
|
|
96
102
|
|
|
97
103
|
```mermaid
|
package/bin/factory
CHANGED
|
@@ -30,6 +30,8 @@ usage() {
|
|
|
30
30
|
' telegram status | configure | start | stop' \
|
|
31
31
|
' workspace list | import PATH|OWNER/REPO [--name NAME] | show NAME | remove NAME' \
|
|
32
32
|
' submit <workspace|owner/repo> <objective>' \
|
|
33
|
+
' update check | now | status | enable | disable' \
|
|
34
|
+
' approval approve | deny OBJECTIVE_ID APPROVAL_ID REASON' \
|
|
33
35
|
' issue <owner/repo> <number>' \
|
|
34
36
|
' ui' \
|
|
35
37
|
' init <local-project-path>' \
|
|
@@ -46,7 +48,7 @@ apply_runtime_model() {
|
|
|
46
48
|
script="set -euo pipefail; exec 9>/run/lock/factory-ai-config.lock; flock -x 9; first=/etc/agent-factory.env; second=/etc/agent-factory-control.env; backup_first=\$(mktemp); backup_second=\$(mktemp); cp --preserve=mode,ownership \"\$first\" \"\$backup_first\"; cp --preserve=mode,ownership \"\$second\" \"\$backup_second\"; rollback() { install -m 0600 -o root -g root \"\$backup_first\" \"\$first\"; install -m 0600 -o root -g root \"\$backup_second\" \"\$second\"; systemctl restart agent-factory-control agent-factory-worker agent-factory-release || true; rm -f \"\$backup_first\" \"\$backup_second\"; }; trap rollback ERR TERM INT; for file in \"\$first\" \"\$second\"; do tmp=\$(mktemp); grep -v '^${variable}=' \"\$file\" > \"\$tmp\" || true; if [[ '$mode' == set ]]; then printf '${variable}=%s\\n' '$value' >> \"\$tmp\"; fi; install -m 0600 -o root -g root \"\$tmp\" \"\$file\"; rm -f \"\$tmp\"; done; systemctl restart agent-factory-control agent-factory-worker agent-factory-release; systemctl is-active --quiet agent-factory-control agent-factory-worker agent-factory-release; trap - ERR TERM INT; rm -f \"\$backup_first\" \"\$backup_second\""
|
|
47
49
|
printf -v script 'bash -c %q' "$script"
|
|
48
50
|
settings=$(jq -n --arg command "$script" '{commandToExecute:$command}')
|
|
49
|
-
az vm extension set --resource-group "$RESOURCE_GROUP" --vm-name "$VM" --name
|
|
51
|
+
az vm extension set --resource-group "$RESOURCE_GROUP" --vm-name "$VM" --name CustomScript \
|
|
50
52
|
--publisher Microsoft.Azure.Extensions --version 2.1 --force-update --protected-settings "$settings" --output none
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -76,7 +78,46 @@ configure_model_transaction() {
|
|
|
76
78
|
trap - EXIT INT TERM
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
vm_extension_command() {
|
|
82
|
+
local script=$1 settings
|
|
83
|
+
printf -v script 'bash -c %q' "$script"
|
|
84
|
+
settings=$(jq -n --arg command "$script" '{commandToExecute:$command}')
|
|
85
|
+
az vm extension set --resource-group "$RESOURCE_GROUP" --vm-name "$VM" --name CustomScript \
|
|
86
|
+
--publisher Microsoft.Azure.Extensions --version 2.1 --force-update --protected-settings "$settings" --output none
|
|
87
|
+
az vm extension show --resource-group "$RESOURCE_GROUP" --vm-name "$VM" --name CustomScript --instance-view --query 'instanceView.statuses[0].message' --output tsv
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
case "$command" in
|
|
91
|
+
update)
|
|
92
|
+
action=${1:-check}
|
|
93
|
+
case "$action" in
|
|
94
|
+
check)
|
|
95
|
+
current=$(node -p "require('$FACTORY_ROOT/package.json').version")
|
|
96
|
+
latest=$(npm view factory-ai version)
|
|
97
|
+
printf 'Local %s · latest %s\n' "$current" "$latest"
|
|
98
|
+
;;
|
|
99
|
+
now)
|
|
100
|
+
vm_extension_command 'bash /opt/agent-factory/app/bootstrap/auto-update.sh'
|
|
101
|
+
latest=$(npm view factory-ai version)
|
|
102
|
+
current=$(node -p "require('$FACTORY_ROOT/package.json').version")
|
|
103
|
+
if [[ $current != "$latest" ]]; then npm install --global "factory-ai@$latest"; fi
|
|
104
|
+
printf 'Factory AI is current at %s locally and Azure update verification completed.\n' "$latest"
|
|
105
|
+
;;
|
|
106
|
+
status)
|
|
107
|
+
vm_extension_command 'set -e; printf "timer enabled: "; systemctl is-enabled factory-ai-update.timer; printf "timer active: "; systemctl is-active factory-ai-update.timer; jq -c . /opt/agent-factory/state/runtime-version.json'
|
|
108
|
+
;;
|
|
109
|
+
enable)
|
|
110
|
+
vm_extension_command 'systemctl enable --now factory-ai-update.timer; systemctl is-active factory-ai-update.timer'
|
|
111
|
+
;;
|
|
112
|
+
disable)
|
|
113
|
+
vm_extension_command 'systemctl disable --now factory-ai-update.timer; systemctl is-active factory-ai-update.timer >/dev/null && exit 1 || true; printf "automatic updates disabled\n"'
|
|
114
|
+
;;
|
|
115
|
+
*) printf 'Usage: factory update check | now | status | enable | disable\n' >&2; exit 2 ;;
|
|
116
|
+
esac
|
|
117
|
+
;;
|
|
118
|
+
approval)
|
|
119
|
+
SERVICE_BUS_NAMESPACE="$NAMESPACE" CONTROL_QUEUE=control-events exec node "$FACTORY_ROOT/src/approval-cli.js" "$@"
|
|
120
|
+
;;
|
|
80
121
|
acp)
|
|
81
122
|
request=${1:?ACP request JSON file required}
|
|
82
123
|
SERVICE_BUS_NAMESPACE="$NAMESPACE" CONTROL_QUEUE=control-events FACTORY_ACP_ENABLED=true exec node "$FACTORY_ROOT/src/acp-cli.js" "$request"
|
|
@@ -199,8 +240,8 @@ case "$command" in
|
|
|
199
240
|
if [[ -x $binary ]]; then
|
|
200
241
|
FACTORY_STORAGE_ACCOUNT="$STORAGE" FACTORY_NAME="$FACTORY_NAME" FACTORY_PURPOSE="$FACTORY_PURPOSE" exec "$binary"
|
|
201
242
|
fi
|
|
202
|
-
printf '
|
|
203
|
-
|
|
243
|
+
printf 'Verified Factory TUI binary is unavailable for %s/%s. Check GitHub release assets and `gh auth status`.\n' "$os" "$arch" >&2
|
|
244
|
+
exit 1
|
|
204
245
|
;;
|
|
205
246
|
setup)
|
|
206
247
|
for dependency in az gh jq curl ssh-keygen node; do command -v "$dependency" >/dev/null || { printf 'Missing dependency: %s\n' "$dependency" >&2; exit 1; }; done
|
package/cmd/factory-ui/main.go
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package main
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"bytes"
|
|
4
5
|
"context"
|
|
5
6
|
"encoding/json"
|
|
6
7
|
"fmt"
|
|
@@ -11,6 +12,7 @@ import (
|
|
|
11
12
|
"regexp"
|
|
12
13
|
"strconv"
|
|
13
14
|
"strings"
|
|
15
|
+
"sync"
|
|
14
16
|
"time"
|
|
15
17
|
|
|
16
18
|
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
|
@@ -48,6 +50,12 @@ type objective struct {
|
|
|
48
50
|
Tasks []task `json:"tasks"`
|
|
49
51
|
PullRequest string `json:"pullRequest"`
|
|
50
52
|
Blocker string `json:"blocker"`
|
|
53
|
+
Approval *struct {
|
|
54
|
+
ApprovalID string `json:"approvalId"`
|
|
55
|
+
Status string `json:"status"`
|
|
56
|
+
Policy string `json:"policy"`
|
|
57
|
+
Reason string `json:"reason"`
|
|
58
|
+
} `json:"approval"`
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
type workspace struct {
|
|
@@ -96,22 +104,30 @@ type snapshotMsg struct {
|
|
|
96
104
|
err error
|
|
97
105
|
}
|
|
98
106
|
type tickMsg time.Time
|
|
99
|
-
type
|
|
107
|
+
type commandResultMsg struct {
|
|
108
|
+
command, output string
|
|
109
|
+
err error
|
|
110
|
+
}
|
|
100
111
|
|
|
101
112
|
type model struct {
|
|
102
|
-
client
|
|
103
|
-
factoryName
|
|
104
|
-
purpose
|
|
105
|
-
width
|
|
106
|
-
height
|
|
107
|
-
tab
|
|
108
|
-
scroll
|
|
109
|
-
dashboard
|
|
110
|
-
logs
|
|
111
|
-
workspaces
|
|
112
|
-
workspaceErr
|
|
113
|
-
loading
|
|
114
|
-
err
|
|
113
|
+
client *azblob.Client
|
|
114
|
+
factoryName string
|
|
115
|
+
purpose string
|
|
116
|
+
width int
|
|
117
|
+
height int
|
|
118
|
+
tab int
|
|
119
|
+
scroll int
|
|
120
|
+
dashboard dashboard
|
|
121
|
+
logs string
|
|
122
|
+
workspaces []workspace
|
|
123
|
+
workspaceErr string
|
|
124
|
+
loading bool
|
|
125
|
+
err error
|
|
126
|
+
commandMode bool
|
|
127
|
+
commandInput string
|
|
128
|
+
commandOutput string
|
|
129
|
+
commandHistory []string
|
|
130
|
+
historyIndex int
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
var (
|
|
@@ -126,6 +142,22 @@ var (
|
|
|
126
142
|
ansi = regexp.MustCompile(`\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?)`)
|
|
127
143
|
)
|
|
128
144
|
|
|
145
|
+
type synchronizedBuffer struct {
|
|
146
|
+
mu sync.Mutex
|
|
147
|
+
buffer bytes.Buffer
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func (value *synchronizedBuffer) Write(data []byte) (int, error) {
|
|
151
|
+
value.mu.Lock()
|
|
152
|
+
defer value.mu.Unlock()
|
|
153
|
+
return value.buffer.Write(data)
|
|
154
|
+
}
|
|
155
|
+
func (value *synchronizedBuffer) String() string {
|
|
156
|
+
value.mu.Lock()
|
|
157
|
+
defer value.mu.Unlock()
|
|
158
|
+
return value.buffer.String()
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
func clean(value string) string {
|
|
130
162
|
value = ansi.ReplaceAllString(value, "")
|
|
131
163
|
return strings.Map(func(character rune) rune {
|
|
@@ -136,6 +168,96 @@ func clean(value string) string {
|
|
|
136
168
|
}, value)
|
|
137
169
|
}
|
|
138
170
|
|
|
171
|
+
func parseCommandLine(value string) ([]string, error) {
|
|
172
|
+
var args []string
|
|
173
|
+
var current strings.Builder
|
|
174
|
+
var quote rune
|
|
175
|
+
escaped := false
|
|
176
|
+
flush := func() {
|
|
177
|
+
if current.Len() > 0 {
|
|
178
|
+
args = append(args, current.String())
|
|
179
|
+
current.Reset()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for _, character := range strings.TrimSpace(value) {
|
|
183
|
+
if escaped {
|
|
184
|
+
current.WriteRune(character)
|
|
185
|
+
escaped = false
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if character == '\\' && quote != '\'' {
|
|
189
|
+
escaped = true
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if quote != 0 {
|
|
193
|
+
if character == quote {
|
|
194
|
+
quote = 0
|
|
195
|
+
} else {
|
|
196
|
+
current.WriteRune(character)
|
|
197
|
+
}
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
if character == '\'' || character == '"' {
|
|
201
|
+
quote = character
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
if character == ' ' || character == '\t' {
|
|
205
|
+
flush()
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
current.WriteRune(character)
|
|
209
|
+
}
|
|
210
|
+
if escaped || quote != 0 {
|
|
211
|
+
return nil, fmt.Errorf("unterminated quote or escape")
|
|
212
|
+
}
|
|
213
|
+
flush()
|
|
214
|
+
if len(args) > 0 && args[0] == "factory" {
|
|
215
|
+
args = args[1:]
|
|
216
|
+
}
|
|
217
|
+
return args, nil
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func validateFactoryCommand(args []string) error {
|
|
221
|
+
if len(args) == 0 {
|
|
222
|
+
return fmt.Errorf("enter a factory command")
|
|
223
|
+
}
|
|
224
|
+
allowed := map[string]bool{"setup": true, "configure": true, "models": true, "acp": true, "extension": true, "github": true, "telegram": true, "workspace": true, "submit": true, "issue": true, "init": true, "secret": true, "dashboard": true, "status": true, "queue": true, "report": true, "logs": true, "doctor": true, "pause": true, "resume": true, "update": true, "approval": true, "help": true, "--help": true, "-h": true}
|
|
225
|
+
if args[0] == "ui" {
|
|
226
|
+
return fmt.Errorf("the UI command cannot be launched inside itself")
|
|
227
|
+
}
|
|
228
|
+
if !allowed[args[0]] {
|
|
229
|
+
return fmt.Errorf("unknown factory command: %s", args[0])
|
|
230
|
+
}
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func interactiveFactoryCommand(args []string) bool {
|
|
235
|
+
if len(args) == 0 {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
return args[0] == "setup" || args[0] == "configure" || args[0] == "secret" && len(args) > 1 && args[1] == "set" || args[0] == "telegram" && len(args) > 1 && args[1] == "configure"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func executeFactoryCommand(args []string) tea.Cmd {
|
|
242
|
+
return func() tea.Msg {
|
|
243
|
+
command := "factory " + strings.Join(args, " ")
|
|
244
|
+
output, err := exec.Command("factory", args...).CombinedOutput()
|
|
245
|
+
return commandResultMsg{command: command, output: string(output), err: err}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func interactiveFactoryProcess(args []string) tea.Cmd {
|
|
250
|
+
var transcript synchronizedBuffer
|
|
251
|
+
command := exec.Command("factory", args...)
|
|
252
|
+
command.Stdin = os.Stdin
|
|
253
|
+
command.Stdout = io.MultiWriter(os.Stdout, &transcript)
|
|
254
|
+
command.Stderr = io.MultiWriter(os.Stderr, &transcript)
|
|
255
|
+
label := "factory " + strings.Join(args, " ")
|
|
256
|
+
return tea.ExecProcess(command, func(err error) tea.Msg {
|
|
257
|
+
return commandResultMsg{command: label, output: transcript.String(), err: err}
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
139
261
|
func readConfig() map[string]string {
|
|
140
262
|
result := map[string]string{}
|
|
141
263
|
for _, key := range []string{"FACTORY_STORAGE_ACCOUNT", "FACTORY_NAME", "FACTORY_PURPOSE"} {
|
|
@@ -203,6 +325,57 @@ func (m model) Init() tea.Cmd { return tea.Batch(fetchCmd(m.client), tickCmd())
|
|
|
203
325
|
func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
204
326
|
switch msg := message.(type) {
|
|
205
327
|
case tea.KeyMsg:
|
|
328
|
+
if m.commandMode {
|
|
329
|
+
switch msg.String() {
|
|
330
|
+
case "esc":
|
|
331
|
+
m.commandMode = false
|
|
332
|
+
m.commandInput = ""
|
|
333
|
+
case "enter":
|
|
334
|
+
line := strings.TrimSpace(m.commandInput)
|
|
335
|
+
args, err := parseCommandLine(line)
|
|
336
|
+
if err == nil {
|
|
337
|
+
err = validateFactoryCommand(args)
|
|
338
|
+
}
|
|
339
|
+
if err != nil {
|
|
340
|
+
m.commandOutput = err.Error()
|
|
341
|
+
return m, nil
|
|
342
|
+
}
|
|
343
|
+
m.commandMode = false
|
|
344
|
+
m.commandInput = ""
|
|
345
|
+
m.commandHistory = append(m.commandHistory, line)
|
|
346
|
+
m.historyIndex = len(m.commandHistory)
|
|
347
|
+
if interactiveFactoryCommand(args) {
|
|
348
|
+
return m, interactiveFactoryProcess(args)
|
|
349
|
+
}
|
|
350
|
+
m.loading = true
|
|
351
|
+
return m, executeFactoryCommand(args)
|
|
352
|
+
case "backspace", "ctrl+h":
|
|
353
|
+
runes := []rune(m.commandInput)
|
|
354
|
+
if len(runes) > 0 {
|
|
355
|
+
m.commandInput = string(runes[:len(runes)-1])
|
|
356
|
+
}
|
|
357
|
+
case "ctrl+u":
|
|
358
|
+
m.commandInput = ""
|
|
359
|
+
case "up":
|
|
360
|
+
if m.historyIndex > 0 {
|
|
361
|
+
m.historyIndex--
|
|
362
|
+
m.commandInput = m.commandHistory[m.historyIndex]
|
|
363
|
+
}
|
|
364
|
+
case "down":
|
|
365
|
+
if m.historyIndex < len(m.commandHistory)-1 {
|
|
366
|
+
m.historyIndex++
|
|
367
|
+
m.commandInput = m.commandHistory[m.historyIndex]
|
|
368
|
+
} else {
|
|
369
|
+
m.historyIndex = len(m.commandHistory)
|
|
370
|
+
m.commandInput = ""
|
|
371
|
+
}
|
|
372
|
+
default:
|
|
373
|
+
if len(msg.Runes) > 0 {
|
|
374
|
+
m.commandInput += string(msg.Runes)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return m, nil
|
|
378
|
+
}
|
|
206
379
|
switch msg.String() {
|
|
207
380
|
case "q", "ctrl+c":
|
|
208
381
|
return m, tea.Quit
|
|
@@ -233,8 +406,39 @@ func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
233
406
|
m.scroll += 10
|
|
234
407
|
case "home":
|
|
235
408
|
m.scroll = 0
|
|
236
|
-
case "
|
|
237
|
-
|
|
409
|
+
case ":", "o":
|
|
410
|
+
m.commandMode = true
|
|
411
|
+
m.commandInput = ""
|
|
412
|
+
m.historyIndex = len(m.commandHistory)
|
|
413
|
+
case "n":
|
|
414
|
+
m.commandMode = true
|
|
415
|
+
m.commandInput = "submit "
|
|
416
|
+
m.historyIndex = len(m.commandHistory)
|
|
417
|
+
case "i":
|
|
418
|
+
m.commandMode = true
|
|
419
|
+
m.commandInput = "workspace import "
|
|
420
|
+
m.historyIndex = len(m.commandHistory)
|
|
421
|
+
case "a":
|
|
422
|
+
m.commandMode = true
|
|
423
|
+
m.commandInput = "secret set "
|
|
424
|
+
m.historyIndex = len(m.commandHistory)
|
|
425
|
+
case "y":
|
|
426
|
+
m.commandMode = true
|
|
427
|
+
m.commandInput = "approval approve "
|
|
428
|
+
m.historyIndex = len(m.commandHistory)
|
|
429
|
+
case "x":
|
|
430
|
+
m.commandMode = true
|
|
431
|
+
m.commandInput = "approval deny "
|
|
432
|
+
m.historyIndex = len(m.commandHistory)
|
|
433
|
+
case "p":
|
|
434
|
+
m.loading = true
|
|
435
|
+
return m, executeFactoryCommand([]string{"pause"})
|
|
436
|
+
case "u":
|
|
437
|
+
m.loading = true
|
|
438
|
+
return m, executeFactoryCommand([]string{"resume"})
|
|
439
|
+
case "?":
|
|
440
|
+
m.loading = true
|
|
441
|
+
return m, executeFactoryCommand([]string{"--help"})
|
|
238
442
|
case "r":
|
|
239
443
|
m.loading = true
|
|
240
444
|
return m, fetchCmd(m.client)
|
|
@@ -252,12 +456,16 @@ func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
252
456
|
if msg.err == nil {
|
|
253
457
|
m.dashboard, m.logs, m.workspaces, m.workspaceErr = msg.dashboard, msg.logs, msg.workspaces, msg.workspaceErr
|
|
254
458
|
}
|
|
255
|
-
case
|
|
459
|
+
case commandResultMsg:
|
|
460
|
+
m.loading = false
|
|
256
461
|
m.err = msg.err
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
462
|
+
result := strings.TrimSpace(clean(msg.output))
|
|
463
|
+
if msg.err != nil && result == "" {
|
|
464
|
+
result = msg.err.Error()
|
|
260
465
|
}
|
|
466
|
+
m.commandOutput = fmt.Sprintf("$ %s\n%s", clean(msg.command), result)
|
|
467
|
+
m.scroll = int(^uint(0) >> 1)
|
|
468
|
+
return m, fetchCmd(m.client)
|
|
261
469
|
case tickMsg:
|
|
262
470
|
if !m.loading {
|
|
263
471
|
m.loading = true
|
|
@@ -336,6 +544,9 @@ func (m model) objectives() string {
|
|
|
336
544
|
if item.PullRequest != "" {
|
|
337
545
|
fmt.Fprintf(&value, " PR %s\n", clean(item.PullRequest))
|
|
338
546
|
}
|
|
547
|
+
if item.Approval != nil {
|
|
548
|
+
fmt.Fprintf(&value, " %s approval %s · %s\n ID %s\n", badge(item.Approval.Status), clean(item.Approval.Policy), trim(item.Approval.Reason, 90), clean(item.Approval.ApprovalID))
|
|
549
|
+
}
|
|
339
550
|
value.WriteString("\n")
|
|
340
551
|
}
|
|
341
552
|
return value.String()
|
|
@@ -380,7 +591,7 @@ func (m model) workspaceList() string {
|
|
|
380
591
|
return "Workspace catalog unavailable: " + clean(m.workspaceErr)
|
|
381
592
|
}
|
|
382
593
|
if len(m.workspaces) == 0 {
|
|
383
|
-
return "No workspaces imported. Press
|
|
594
|
+
return "No workspaces imported. Press i and enter a local path or owner/repo."
|
|
384
595
|
}
|
|
385
596
|
var value strings.Builder
|
|
386
597
|
value.WriteString("IMPORTED WORKSPACES\n\n")
|
|
@@ -438,7 +649,11 @@ func (m model) View() string {
|
|
|
438
649
|
if height < 10 {
|
|
439
650
|
height = 10
|
|
440
651
|
}
|
|
441
|
-
|
|
652
|
+
body := m.body()
|
|
653
|
+
if m.commandOutput != "" {
|
|
654
|
+
body += "\n\nCOMMAND OUTPUT\n" + m.commandOutput
|
|
655
|
+
}
|
|
656
|
+
lines := strings.Split(body, "\n")
|
|
442
657
|
visible := height - 2
|
|
443
658
|
maxScroll := len(lines) - visible
|
|
444
659
|
if maxScroll < 0 {
|
|
@@ -453,11 +668,13 @@ func (m model) View() string {
|
|
|
453
668
|
end = len(lines)
|
|
454
669
|
}
|
|
455
670
|
content := lipgloss.NewStyle().Width(width-4).Height(height).Border(lipgloss.RoundedBorder()).BorderForeground(border).Padding(1, 2).Render(strings.Join(lines[scroll:end], "\n"))
|
|
456
|
-
footer := "
|
|
457
|
-
if m.
|
|
671
|
+
footer := ": command · n submit · i import · ? help · q quit"
|
|
672
|
+
if m.commandMode {
|
|
673
|
+
footer = lipgloss.NewStyle().Foreground(accent).Bold(true).Render(": ") + clean(m.commandInput) + "█"
|
|
674
|
+
} else if m.loading {
|
|
458
675
|
footer = "Refreshing snapshot…"
|
|
459
676
|
}
|
|
460
|
-
if m.err != nil {
|
|
677
|
+
if m.err != nil && !m.commandMode {
|
|
461
678
|
footer = lipgloss.NewStyle().Foreground(danger).Render(m.err.Error())
|
|
462
679
|
}
|
|
463
680
|
return lipgloss.JoinVertical(lipgloss.Left, header, tabLine, content, lipgloss.NewStyle().Foreground(muted).Render(footer))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factory-ai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Deploy a private autonomous coding-agent factory on Azure: isolated builders, testers, security reviewers, durable orchestration, multi-model routing, memory, cost controls, and gated GitHub pull requests.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
},
|
|
33
33
|
"bin": {
|
|
34
34
|
"factory": "bin/factory",
|
|
35
|
-
"factory-ui": "src/tui.js",
|
|
36
35
|
"agent-factory-ceo": "src/ceo.js",
|
|
37
36
|
"agent-factory-control": "src/control-service.js",
|
|
38
37
|
"agent-factory-dashboard": "src/dashboard.js",
|
|
@@ -74,7 +73,6 @@
|
|
|
74
73
|
"@modelcontextprotocol/server-memory": "2026.7.4",
|
|
75
74
|
"@playwright/mcp": "0.0.78",
|
|
76
75
|
"@upstash/context7-mcp": "3.2.3",
|
|
77
|
-
"neo-blessed": "0.2.0",
|
|
78
76
|
"zod": "3.25.76"
|
|
79
77
|
},
|
|
80
78
|
"devDependencies": {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
3
|
+
import { ServiceBusClient } from "@azure/service-bus";
|
|
4
|
+
import { sendMessage } from "./bus.js";
|
|
5
|
+
|
|
6
|
+
const [action, objectiveId, approvalId, ...reasonWords] = process.argv.slice(2);
|
|
7
|
+
const decision = action === "approve" ? "approved" : action === "deny" ? "denied" : null;
|
|
8
|
+
const reason = reasonWords.join(" ").trim();
|
|
9
|
+
if (!decision || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(objectiveId ?? "") || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(approvalId ?? "") || !reason) throw new Error("Usage: factory approval approve|deny OBJECTIVE_ID APPROVAL_ID REASON");
|
|
10
|
+
const namespace = process.env.SERVICE_BUS_NAMESPACE;
|
|
11
|
+
if (!namespace) throw new Error("SERVICE_BUS_NAMESPACE is required");
|
|
12
|
+
const client = new ServiceBusClient(namespace.includes(".") ? namespace : `${namespace}.servicebus.windows.net`, new DefaultAzureCredential());
|
|
13
|
+
const sender = client.createSender(process.env.CONTROL_QUEUE ?? "control-events");
|
|
14
|
+
const messageId = `approval-${objectiveId}-${approvalId}-${decision}`.slice(0, 64);
|
|
15
|
+
try { await sendMessage(sender, { type: "approval_decision", objectiveId, approvalId, decision, actor: "local-operator", reason: reason.slice(0, 1000), decidedAt: new Date().toISOString(), messageId }, messageId, objectiveId); }
|
|
16
|
+
finally { await sender.close(); await client.close(); }
|
|
17
|
+
process.stdout.write(`${JSON.stringify({ objectiveId, approvalId, decision })}\n`);
|
package/src/tui.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import blessed from "neo-blessed";
|
|
3
|
-
import { createOperator } from "./operator.js";
|
|
4
|
-
|
|
5
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Factory AI UI requires an interactive terminal");
|
|
6
|
-
|
|
7
|
-
const operator = createOperator();
|
|
8
|
-
const factoryName = process.env.FACTORY_NAME ?? "Factory AI";
|
|
9
|
-
const factoryPurpose = process.env.FACTORY_PURPOSE ?? "Ship secure reviewed software continuously";
|
|
10
|
-
const screen = blessed.screen({ smartCSR: true, title: factoryName, fullUnicode: true, dockBorders: true });
|
|
11
|
-
const colors = { bg: "#0d0f12", panel: "#15191f", border: "#303743", text: "#d9e0e8", muted: "#7f8b99", accent: "#78dba9", warn: "#efc46b", danger: "#ef7d7d", blue: "#77a8ff" };
|
|
12
|
-
|
|
13
|
-
blessed.box({ parent: screen, top: 0, left: 0, width: "100%", height: 3, tags: true, style: { bg: colors.panel, fg: colors.text }, content: ` {bold}{#78dba9-fg}${factoryName.toUpperCase()}{/} {/bold} ${factoryPurpose}` });
|
|
14
|
-
const menu = blessed.list({ parent: screen, top: 3, left: 0, width: 23, bottom: 2, border: { type: "line" }, label: " Navigate ", keys: true, mouse: true, vi: true, items: ["Overview", "Workspaces", "Objectives", "Agents", "Secrets", "Capabilities", "Logs", "Settings"], style: { bg: colors.panel, fg: colors.text, border: { fg: colors.border }, selected: { bg: colors.accent, fg: "#07130d", bold: true }, item: { fg: colors.text } } });
|
|
15
|
-
const main = blessed.box({ parent: screen, top: 3, left: 23, right: 0, bottom: 2, border: { type: "line" }, label: " Overview ", tags: true, scrollable: true, alwaysScroll: true, keys: true, mouse: true, vi: true, scrollbar: { ch: "▐", style: { fg: colors.accent } }, padding: { left: 2, right: 2 }, style: { bg: colors.bg, fg: colors.text, border: { fg: colors.border } } });
|
|
16
|
-
const footer = blessed.box({ parent: screen, bottom: 0, left: 0, width: "100%", height: 2, tags: true, style: { bg: colors.panel, fg: colors.muted }, content: " {bold}n{/} new {bold}i{/} import workspace {bold}r{/} refresh {bold}a{/} secret {bold}y/x{/} approve/deny {bold}q{/} quit" });
|
|
17
|
-
|
|
18
|
-
let dashboard;
|
|
19
|
-
let capabilities;
|
|
20
|
-
let secrets;
|
|
21
|
-
let logs;
|
|
22
|
-
let workspaces = [];
|
|
23
|
-
let section = "Overview";
|
|
24
|
-
let refreshing = false;
|
|
25
|
-
|
|
26
|
-
function safe(value) {
|
|
27
|
-
const text = String(value ?? "").replaceAll(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
|
|
28
|
-
return blessed.escape ? blessed.escape(text) : text.replaceAll("{", "\\{").replaceAll("}", "\\}");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function status(message, color = colors.muted) {
|
|
32
|
-
footer.setContent(` {${color}-fg}${message}{/} {bold}n{/} new {bold}i{/} import {bold}r{/} refresh {bold}q{/} quit`);
|
|
33
|
-
screen.render();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function badge(value) {
|
|
37
|
-
const color = value === "succeeded" || value === "complete" ? colors.accent : value === "failed" || value === "blocked" ? colors.danger : value === "running" ? colors.blue : colors.warn;
|
|
38
|
-
return `{${color}-fg}${value}{/}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function renderOverview() {
|
|
42
|
-
const cost = dashboard?.cost ? `${dashboard.cost.currency} ${dashboard.cost.monthToDate.toFixed(2)}` : "unavailable";
|
|
43
|
-
const counts = Object.entries(dashboard?.summary?.objectives ?? {}).map(([key, value]) => `${key} ${value}`).join(" ") || "none";
|
|
44
|
-
const modelUsage = Object.values(dashboard?.modelUsage ?? {});
|
|
45
|
-
const inputTokens = modelUsage.reduce((sum, item) => sum + item.inputTokens, 0);
|
|
46
|
-
const cachedTokens = modelUsage.reduce((sum, item) => sum + item.cachedInputTokens, 0);
|
|
47
|
-
const outputTokens = modelUsage.reduce((sum, item) => sum + item.outputTokens, 0);
|
|
48
|
-
const recent = (dashboard?.objectives ?? []).slice(-8).reverse().map((objective) => ` ${badge(objective.status.padEnd(9))} {bold}${safe(objective.objective)}{/}\n ${safe(objective.repository ?? "")}`).join("\n\n");
|
|
49
|
-
main.setContent(`{bold}System{/}\n\n Queue {#78dba9-fg}${dashboard?.queue?.active ?? 0}{/}\n Dead letter ${dashboard?.queue?.deadLetter ?? 0}\n Azure MTD {#efc46b-fg}${cost}{/}\n Objectives ${counts}\n Tokens ${inputTokens} in · ${cachedTokens} cached · ${outputTokens} out\n\n{bold}Recent objectives{/}\n\n${recent || " No objectives yet."}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function renderObjectives() {
|
|
53
|
-
main.setContent((dashboard?.objectives ?? []).slice().reverse().map((objective) => {
|
|
54
|
-
const tasks = objective.tasks.map((task) => ` ${badge((task.stale ? "stale" : task.state).padEnd(9))} ${task.role.padEnd(9)} {#7f8b99-fg}${safe(task.model)}{/}\n ${safe(task.title)}${task.activity ? ` · ${safe(task.activity.type)}${task.activity.tool ? ` ${safe(task.activity.tool)}` : ""}` : ""}`).join("\n");
|
|
55
|
-
return `${badge(objective.status)} {bold}${safe(objective.objective)}{/}\n ${safe(objective.id)}\n${tasks}${objective.pullRequest ? `\n PR ${safe(objective.pullRequest)}` : ""}${objective.blocker ? `\n {#ef7d7d-fg}${safe(objective.blocker)}{/}` : ""}`;
|
|
56
|
-
}).join("\n\n────────────────────────────────────────────────────────\n\n") || "No objectives." );
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function renderAgents() {
|
|
60
|
-
const agents = (dashboard?.objectives ?? []).flatMap((objective) => objective.tasks.map((task) => ({ objective: objective.objective, ...task }))).filter((task) => !["succeeded"].includes(task.state));
|
|
61
|
-
main.setContent(agents.map((task) => `${badge((task.stale ? "stale" : task.state).padEnd(9))} {bold}${safe(task.role)}{/} ${safe(task.model)}\n ${safe(task.title)}\n ${task.activity ? `${safe(task.activity.phase ?? task.activity.type)}${task.activity.tool ? ` · ${safe(task.activity.tool)}` : ""} · ${Math.round(task.activityAgeSeconds ?? 0)}s ago${task.retries ? ` · ${task.retries} retries` : ""}` : "No activity event yet"}${task.lastError ? `\n {#ef7d7d-fg}${safe(task.lastError)}{/}` : ""}\n {#7f8b99-fg}${safe(task.objective)}{/}`).join("\n\n") || "No active agents.");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function renderSecrets() {
|
|
65
|
-
main.setContent(`{bold}Global Azure Key Vault{/}\n{#7f8b99-fg}Values are never displayed. Press a to add or rotate a secret.{/}\n\n${(secrets ?? []).map((item) => ` {#78dba9-fg}●{/} ${item.name.padEnd(55)} ${item.updated ?? ""}`).join("\n") || " Loading or empty."}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function renderCapabilities() {
|
|
69
|
-
const skills = Object.entries(capabilities?.skills ?? {}).map(([name, item]) => ` {#77a8ff-fg}skill{/} ${name.padEnd(36)} ${item.roles.join(", ")}`).join("\n");
|
|
70
|
-
const mcp = Object.entries(capabilities?.mcp ?? {}).map(([name, item]) => ` {#78dba9-fg}mcp{/} ${name.padEnd(36)} ${item.roles.join(", ")}`).join("\n");
|
|
71
|
-
main.setContent(`{bold}Skills{/}\n\n${skills}\n\n{bold}MCP servers{/}\n\n${mcp}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function renderSettings() {
|
|
75
|
-
const config = operator.config();
|
|
76
|
-
main.setContent(`{bold}Runtime{/}\n\n Resource group ${config.resourceGroup}\n VM ${config.vm}\n Service Bus ${config.namespace}\n Key Vault ${config.vault}\n Operator state ${config.storageAccount || "Run Command fallback"}\n\n{bold}Model policy{/}\n\n Scout GPT-5.4 nano\n Simple builder Kimi K2.7-Code\n Builder GPT-5.5\n Tester GPT-5.4\n Critical roles GPT-5.6\n\n{#7f8b99-fg}Run factory setup to change providers or role routes.{/}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function renderWorkspaces() {
|
|
80
|
-
main.setContent(`{bold}Imported workspaces{/}\n{#7f8b99-fg}Press i to import a local path or owner/repo. Press n to submit work by name.{/}\n\n${workspaces.map((workspace) => ` {#78dba9-fg}●{/} {bold}${safe(workspace.name)}{/} ${safe(workspace.repository)}\n ${safe(workspace.localPath ?? "remote only")} · ${safe(workspace.baseBranch)}`).join("\n\n") || " No workspaces imported."}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function render() {
|
|
84
|
-
main.setLabel(` ${section} `);
|
|
85
|
-
if (section === "Overview") renderOverview();
|
|
86
|
-
else if (section === "Workspaces") renderWorkspaces();
|
|
87
|
-
else if (section === "Objectives") renderObjectives();
|
|
88
|
-
else if (section === "Agents") renderAgents();
|
|
89
|
-
else if (section === "Secrets") renderSecrets();
|
|
90
|
-
else if (section === "Capabilities") renderCapabilities();
|
|
91
|
-
else if (section === "Logs") main.setContent(safe(logs ?? "Loading logs..."));
|
|
92
|
-
else renderSettings();
|
|
93
|
-
main.setScrollPerc(0);
|
|
94
|
-
screen.render();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function refresh() {
|
|
98
|
-
if (refreshing) return;
|
|
99
|
-
refreshing = true;
|
|
100
|
-
status("Refreshing...");
|
|
101
|
-
try {
|
|
102
|
-
dashboard = await operator.dashboard();
|
|
103
|
-
workspaces = await operator.listWorkspaces();
|
|
104
|
-
if (section === "Capabilities" && !capabilities) capabilities = await operator.capabilities();
|
|
105
|
-
if (section === "Secrets") secrets = dashboard?.secrets ?? await operator.listSecrets();
|
|
106
|
-
if (section === "Logs") logs = await operator.logs();
|
|
107
|
-
render();
|
|
108
|
-
status(`Updated ${new Date().toLocaleTimeString()}`);
|
|
109
|
-
} catch (error) { status(error.message, colors.danger); }
|
|
110
|
-
finally { refreshing = false; }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function ask(label, { secret = false } = {}) {
|
|
114
|
-
return new Promise((resolve) => {
|
|
115
|
-
const prompt = blessed.textbox({ parent: screen, top: "center", left: "center", width: "70%", height: 5, border: { type: "line" }, label: ` ${label} `, inputOnFocus: true, keys: true, mouse: true, censor: secret, style: { bg: colors.panel, fg: colors.text, border: { fg: colors.accent } } });
|
|
116
|
-
prompt.focus();
|
|
117
|
-
prompt.readInput((_, value) => { prompt.destroy(); screen.render(); resolve(value?.trim() ?? ""); });
|
|
118
|
-
screen.render();
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function submitObjective() {
|
|
123
|
-
const repository = await ask(`Workspace name${workspaces.length ? ` (${workspaces.map((item) => item.name).join(", ")})` : " or owner/repo"}`);
|
|
124
|
-
if (!repository) return;
|
|
125
|
-
const objective = await ask("CEO objective");
|
|
126
|
-
if (!objective) return;
|
|
127
|
-
status("Submitting objective...");
|
|
128
|
-
try { const result = await operator.submit(repository, objective); status(`Queued ${result.objectiveId ?? "objective"}`, colors.accent); await refresh(); } catch (error) { status(error.message, colors.danger); }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function importWorkspace() {
|
|
132
|
-
const source = await ask("Local path or GitHub owner/repo"); if (!source) return;
|
|
133
|
-
const name = await ask("Workspace name (Enter for repository name)");
|
|
134
|
-
try { const workspace = await operator.importWorkspace(source, name); workspaces = await operator.listWorkspaces(); status(`Imported ${workspace.name}`, colors.accent); section = "Workspaces"; render(); } catch (error) { status(error.message, colors.danger); }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function addSecret() {
|
|
138
|
-
if (section !== "Secrets") return status("Open Secrets before adding a key", colors.warn);
|
|
139
|
-
const name = await ask("Secret name");
|
|
140
|
-
if (!name) return;
|
|
141
|
-
const value = await ask("Secret value", { secret: true });
|
|
142
|
-
if (!value) return;
|
|
143
|
-
status("Storing secret...");
|
|
144
|
-
try {
|
|
145
|
-
await operator.setSecret(name, value);
|
|
146
|
-
secrets = [...(secrets ?? []).filter((item) => item.name !== name), { name, updated: new Date().toISOString() }].sort((left, right) => left.name.localeCompare(right.name));
|
|
147
|
-
renderSecrets();
|
|
148
|
-
status(`Stored ${name}`, colors.accent);
|
|
149
|
-
} catch (error) { status(error.message, colors.danger); }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function decideApproval(decision) {
|
|
153
|
-
const objectiveId = await ask("Objective ID"); if (!objectiveId) return;
|
|
154
|
-
const approvalId = await ask("Approval ID"); if (!approvalId) return;
|
|
155
|
-
const reason = await ask(`${decision === "approved" ? "Approval" : "Denial"} reason`); if (!reason) return;
|
|
156
|
-
try { await operator.approval({ objectiveId, approvalId, decision, reason }); status(`Approval ${decision}`, decision === "approved" ? colors.accent : colors.danger); await refresh(); } catch (error) { status(error.message, colors.danger); }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
menu.on("select", async (_, index) => { section = menu.getItem(index).getText(); render(); await refresh(); menu.focus(); });
|
|
160
|
-
screen.key(["q", "C-c"], () => process.exit(0));
|
|
161
|
-
screen.key("r", refresh);
|
|
162
|
-
screen.key("n", submitObjective);
|
|
163
|
-
screen.key("i", importWorkspace);
|
|
164
|
-
screen.key("a", addSecret);
|
|
165
|
-
screen.key("y", () => decideApproval("approved"));
|
|
166
|
-
screen.key("x", () => decideApproval("denied"));
|
|
167
|
-
screen.key("p", async () => { status("Pausing workers..."); try { await operator.control("pause"); status("Workers paused", colors.warn); } catch (error) { status(error.message, colors.danger); } });
|
|
168
|
-
screen.key("u", async () => { status("Resuming workers..."); try { await operator.control("resume"); status("Workers active", colors.accent); } catch (error) { status(error.message, colors.danger); } });
|
|
169
|
-
screen.key("tab", () => menu.focus());
|
|
170
|
-
menu.focus();
|
|
171
|
-
screen.render();
|
|
172
|
-
await refresh();
|
|
173
|
-
setInterval(refresh, 30_000).unref();
|