@moretea-labs/forge 1.5.1 → 1.6.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 +20 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +29 -19
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/transports/http.ts +25 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/release-materialize.ts +143 -19
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# Controller Runtime System Overview
|
|
2
2
|
|
|
3
3
|
> Status: **Runtime Authority**
|
|
4
|
+
>
|
|
5
|
+
> Control convergence: ChatGPT/human Controller owns semantic understanding and commits Direct/Process/Durable Work/Plan only after sufficient context; Forge does not classify every request at ingress. `RouteDecision` owns execution mechanics after that commitment, while unrelated Work/Plan state never upgrades or blocks ordinary Direct execution. Semantic ownership, execution depth, and resource placement stay independent. CodeGraph/lexical/Forge relations are bounded impact evidence, not another decision authority.
|
|
4
6
|
|
|
5
7
|
## 1. System Definition
|
|
6
8
|
|
|
7
|
-
Forge Controller Runtime is an Agent Engineering Control Plane for one or more local Git repositories. ChatGPT, Local UI, CLI and
|
|
9
|
+
Forge Controller Runtime is an Agent Engineering Control Plane for one or more local Git repositories. ChatGPT is the primary semantic Controller: it may inspect first, then choose whether a request needs only a Direct action, a Process, finite durable Work, a Plan, or a Workflow. Local UI, CLI and integrations can submit the same explicit decisions and commands. Forge supplies bounded context, hard policy/resource constraints, persistence, execution and machine evidence; it does not replace the Controller with prompt classifiers or Plan/Work ceremony.
|
|
10
|
+
|
|
11
|
+
### 1.1 End-to-end authority map
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
User request
|
|
15
|
+
-> ChatGPT / human Controller
|
|
16
|
+
-> Explore when needed: rh_context + raw reads + CodeGraph/Runtime/history evidence
|
|
17
|
+
-> semantic commitment: answer | Direct | Process | finite Work | Plan | Workflow
|
|
18
|
+
-> Forge execution boundary
|
|
19
|
+
-> hard policy / authorization / claims / leases / fencing
|
|
20
|
+
-> placement and executor mechanics; no semantic reinterpretation
|
|
21
|
+
-> execution
|
|
22
|
+
-> Direct mutation: SafePatch -> EditSession -> MutationGate
|
|
23
|
+
-> command/check: Unified Process Runtime
|
|
24
|
+
-> durable responsibility: finite Work using the same execution primitives
|
|
25
|
+
-> recurrence: Workflow/Schedule -> bounded Occurrence -> Controller decision
|
|
26
|
+
-> machine evidence
|
|
27
|
+
-> diff / Process receipt / check / delivery / cleanup facts
|
|
28
|
+
-> ChatGPT / user acceptance
|
|
29
|
+
-> accept, continue, replan, or close the semantic goal
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Authority rule: Context providers return evidence, RoutePolicy selects mechanics from a Controller commitment, Plan/Requirement/Work persist explicitly chosen durable semantics, and Process/EditSession/Evidence record execution facts. None of those layers may infer that a missing Plan/Work makes ordinary implementation unauthorized, or that a successful/failed cleanup receipt by itself proves the user's goal accepted/rejected.
|
|
8
33
|
|
|
9
34
|
## 2. Canonical Runtime Topology
|
|
10
35
|
|
|
@@ -174,6 +174,16 @@ bounded child process (process group) / yielded search
|
|
|
174
174
|
- Timeout/cancel: SIGTERM process group → grace → SIGKILL via `terminateProcessTree`.
|
|
175
175
|
- stdout/stderr collectors cap while streaming.
|
|
176
176
|
|
|
177
|
+
## MCP tool-surface freshness
|
|
178
|
+
|
|
179
|
+
The Canonical Runtime `tools/list` schema fingerprint is the tool-surface identity. Release identity alone does not invalidate a session, while any exposed schema change does.
|
|
180
|
+
|
|
181
|
+
HTTP MCP sessions capture the exact Canonical Runtime fingerprint and tool names at initialize time. The Gateway also observes the existing atomic `runtime/status.json` publication; when its published tool-surface fingerprint changes, it sends the standard MCP `notifications/tools/list_changed` notification to connected sessions whose captured fingerprint is stale. This is event-driven over the existing Runtime status publication—there is no schema poller, refresh daemon, or extra control-plane entity.
|
|
182
|
+
|
|
183
|
+
The notification is an optimization hint, not authority. It never rewrites a session's captured fingerprint. Until the client re-runs discovery/reinitializes, the existing per-request fingerprint fence still rejects stale-schema execution. A lost notification therefore cannot make an obsolete tool call valid; it only risks falling back to the deterministic reinitialize path.
|
|
184
|
+
|
|
185
|
+
This removes the normal stale-tool extra round trip for clients that honor standard MCP list-changed notifications while retaining the same fail-closed schema fence for clients that do not.
|
|
186
|
+
|
|
177
187
|
## Process output confidentiality
|
|
178
188
|
|
|
179
189
|
Process Runtime applies one shared bounded redaction policy before stdout/stderr
|
|
@@ -54,7 +54,15 @@ A routine stores the user's natural-language goal rather than a traditional GUI
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Routine execution and long-lived goal continuation use the existing Schedule Engine rather than a second assistant daemon. Semantic/model-backed routine logic remains external-controller-owned; the Kernel does not execute an unbounded model loop on a timer.
|
|
58
|
+
|
|
59
|
+
For an accepted non-terminal Work, ChatGPT can create a bounded `rh_work.schedule_create` policy in one of three modes:
|
|
60
|
+
|
|
61
|
+
- `continuation` — wake the bound ChatGPT/Codex/Claude/Grok Controller at the configured trigger;
|
|
62
|
+
- `browser_watch` — silently refresh/read an allowed browser target, fingerprint a deterministic text projection, and wake the bound Controller only when the observation changes;
|
|
63
|
+
- `browser_keepalive` — silently refresh an authenticated browser target and wake only when configured login markers indicate that user authentication is required.
|
|
64
|
+
|
|
65
|
+
The first `browser_watch` observation is a silent baseline by default. Unchanged observations do not create a ChatGPT conversation turn. When the bound Work becomes terminal, Forge disables the Schedule before performing another external probe, so temporary watchers naturally disappear with the Goal they serve.
|
|
58
66
|
|
|
59
67
|
## Policy defaults
|
|
60
68
|
|
|
@@ -76,8 +84,10 @@ Requires human approval:
|
|
|
76
84
|
|
|
77
85
|
## Assistant Inbox
|
|
78
86
|
|
|
79
|
-
Assistant Inbox is the user-facing result surface for routine outputs and intent execution records. ChatGPT can read
|
|
87
|
+
Assistant Inbox is the user-facing result surface for meaningful routine outputs, approval/authentication needs, and intent execution records. A successful scheduled probe with no external change should remain silent rather than generating a daily "completed" notification. ChatGPT can read meaningful items and then present the result conversationally, for example: "今天助理整理了什么?"
|
|
80
88
|
|
|
81
89
|
## Current limitation
|
|
82
90
|
|
|
83
91
|
The runtime does not embed a full NLP model. ChatGPT Actions should provide structure when precision matters. The local runtime includes conservative intent inference only for routine creation, reminders, and immediate mail-summary collection.
|
|
92
|
+
|
|
93
|
+
`browser_watch` is a fallback observation source for services without a usable API or webhook; it depends on a live authenticated browser session and caller-supplied selectors/terms when the page contains unrelated or volatile content. Browser refresh can reduce inactivity-related logout but cannot override a provider's absolute session lifetime, security checks, or MFA policy. Provider-native APIs/events should replace browser probes when they are available and more reliable.
|
|
@@ -48,7 +48,7 @@ Session metadata is reusable across actions via `session_id`. Any action that re
|
|
|
48
48
|
- Artifacts stay under `.forge/browser/**` (not arbitrary local paths).
|
|
49
49
|
- CDP attach is bounded to configured loopback endpoints only; the plugin does not scan arbitrary ports or remote hosts. Native discovery checks only the configured Chrome/Vivaldi candidates and does not launch them.
|
|
50
50
|
- CDP browsers are disconnected after the action; Apple Events keeps plugin-owned session tabs open until `close_session`/`close_page` while preserving user-owned tabs; managed contexts are closed after the action. Standard native DOM reads/interactions do not foreground the owned tab and return DOM evidence instead of attempting a screenshot.
|
|
51
|
-
- Health `userFacingStatus` reports `ready`, `
|
|
51
|
+
- Health `userFacingStatus` reports `ready`, `session active`, or setup states.
|
|
52
52
|
|
|
53
53
|
## Configuration
|
|
54
54
|
|
|
@@ -67,12 +67,11 @@ Example:
|
|
|
67
67
|
"nativeBrowserCandidates": ["vivaldi", "chrome"],
|
|
68
68
|
"cdpAttachFallback": "managed_persistent",
|
|
69
69
|
"browserChannel": "chromium",
|
|
70
|
-
"defaultTimeoutMs": 30000
|
|
71
|
-
"allowedDomains": ["example.com", "docs.example.com"]
|
|
70
|
+
"defaultTimeoutMs": 30000
|
|
72
71
|
}
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
Browser URLs are open by default across hosts. The URL boundary is scheme-based: top-level navigation accepts absolute `http://` and `https://` URLs and rejects `file://`, `data:`, browser-internal pages, and custom schemes. Interaction risk and authorization are enforced by the typed action policy rather than a domain grant.
|
|
76
75
|
|
|
77
76
|
Additional browser/profile fields:
|
|
78
77
|
|
|
@@ -131,8 +130,7 @@ Example custom Chrome automation-profile binding:
|
|
|
131
130
|
"browserChannel": "chrome",
|
|
132
131
|
"cdpEndpoint": "http://127.0.0.1:9222",
|
|
133
132
|
"cdpAttachFallback": "fail_closed",
|
|
134
|
-
"defaultTimeoutMs": 30000
|
|
135
|
-
"allowedDomains": ["appstoreconnect.apple.com"]
|
|
133
|
+
"defaultTimeoutMs": 30000
|
|
136
134
|
}
|
|
137
135
|
```
|
|
138
136
|
|
|
@@ -152,12 +150,11 @@ Use native attachment when the target session is already logged in:
|
|
|
152
150
|
"browserMode": "attach_preferred",
|
|
153
151
|
"nativeAttachMode": "auto",
|
|
154
152
|
"nativeBrowserCandidates": ["vivaldi", "chrome"],
|
|
155
|
-
"cdpAttachFallback": "managed_persistent"
|
|
156
|
-
"allowedDomains": ["chatgpt.com", "example.com"]
|
|
153
|
+
"cdpAttachFallback": "managed_persistent"
|
|
157
154
|
}
|
|
158
155
|
```
|
|
159
156
|
|
|
160
|
-
The attach order is CDP, then Apple Events, then managed fallback. macOS may require Automation permission for the controller/Node process to control Vivaldi or Google Chrome. The browser must also permit JavaScript from Apple Events for DOM extraction and interaction. If either permission is unavailable or the browser has no window, the attempt is recorded and fallback policy applies. The provider uses the active browser only for discovery, then operates on a plugin-owned tab identified by stable window/tab metadata. It does not read cookies or storage
|
|
157
|
+
The attach order is CDP, then Apple Events, then managed fallback. macOS may require Automation permission for the controller/Node process to control Vivaldi or Google Chrome. The browser must also permit JavaScript from Apple Events for DOM extraction and interaction. If either permission is unavailable or the browser has no window, the attempt is recorded and fallback policy applies. The provider uses the active browser only for discovery, then operates on a plugin-owned tab identified by stable window/tab metadata. It does not read cookies or storage. Top-level Browser actions accept any absolute HTTP(S) URL; non-HTTP(S) schemes fail before launch.
|
|
161
158
|
|
|
162
159
|
Native limitations are explicit: console/network event capture is unavailable and native file-input/download semantics are not equivalent to Playwright. A screenshot of a plugin-owned background tab fails closed with `PLUGIN_BROWSER_FOREGROUND_REQUIRED`; Forge does not silently activate the tab just to capture evidence. Use explicit foreground/human handoff, CDP, or a managed context for workflows that require screenshots or capabilities not available through background DOM automation.
|
|
163
160
|
|
|
@@ -219,15 +216,14 @@ Risk levels:
|
|
|
219
216
|
- `click`, `type`, `press` use `risk=remote_write`
|
|
220
217
|
- `wait_for_selector` uses `risk=workspace_write`
|
|
221
218
|
|
|
222
|
-
##
|
|
223
|
-
|
|
224
|
-
The plugin enforces `allowedDomains` in three places:
|
|
219
|
+
## URL scheme boundary
|
|
225
220
|
|
|
226
|
-
|
|
227
|
-
2. It validates any saved `session_id` target before interaction.
|
|
228
|
-
3. It re-checks the resulting page URL after the action and rejects the result if navigation leaves the allowed set.
|
|
221
|
+
Browser no longer maintains per-domain grants or a host allowlist. Any absolute HTTP(S) URL may be opened, redirected to, or reached by an interaction. This avoids configuration churn for normal browsing while preserving the actual trust boundaries:
|
|
229
222
|
|
|
230
|
-
|
|
223
|
+
1. top-level Browser targets must use `http:` or `https:`;
|
|
224
|
+
2. local files, browser-internal URLs, `data:` URLs, and custom schemes are rejected as Browser targets;
|
|
225
|
+
3. action risk and confirmation still govern remote writes and interactive operations;
|
|
226
|
+
4. silent native automation stays on plugin-owned background tabs unless an explicit human handoff requires foreground presentation.
|
|
231
227
|
|
|
232
228
|
## Dependency requirement
|
|
233
229
|
|
|
@@ -24,7 +24,7 @@ These features are part of the default local setup:
|
|
|
24
24
|
| Codex / Claude | Delegated implementation for work too large for Direct Edit | Install and authenticate the corresponding CLI; explicitly enable the dev runner. |
|
|
25
25
|
| GitHub | Issue/Project synchronization, PR workflows, cloud-agent sessions | Authenticated `gh` and repository permission. |
|
|
26
26
|
| Tailscale / Cloudflare | Stable HTTPS endpoint for ChatGPT MCP | Tunnel client and appropriate account/domain configuration. |
|
|
27
|
-
| Browser | Playwright navigation, screenshots, and bounded browser evidence | Browser binaries and
|
|
27
|
+
| Browser | Playwright navigation, screenshots, and bounded browser evidence | Browser binaries and HTTP(S) URL scheme validation. |
|
|
28
28
|
| CodeGraph | Additional code relationships and impact navigation | CodeGraph CLI; native Windows automatic setup is not yet enabled. |
|
|
29
29
|
| Google Workspace | Gmail and Calendar assistant actions | Explicit OAuth setup and plugin permission. |
|
|
30
30
|
| Schedules and findings | Supervised recurring checks and candidate findings | Canonical Forge Runtime running; live actions remain policy-gated. |
|
|
@@ -6,8 +6,8 @@ Homebrew is planned as a convenience layer after the first stable npm/GitHub rel
|
|
|
6
6
|
|
|
7
7
|
Do not publish a formula until all of the following are true:
|
|
8
8
|
|
|
9
|
-
- `@moretea-labs/forge@1.
|
|
10
|
-
- Git tag and GitHub Release `v1.
|
|
9
|
+
- `@moretea-labs/forge@1.6.0` exists on npm;
|
|
10
|
+
- Git tag and GitHub Release `v1.6.0` exist and point to the tested commit;
|
|
11
11
|
- the npm tarball has an immutable checksum;
|
|
12
12
|
- macOS installation and `forge --version` / `forge doctor` tests pass;
|
|
13
13
|
- upgrade and uninstall behavior is documented.
|
|
@@ -34,7 +34,7 @@ For Forge source development only:
|
|
|
34
34
|
```bash
|
|
35
35
|
git clone https://github.com/moretea-labs/forge.git
|
|
36
36
|
cd forge
|
|
37
|
-
|
|
37
|
+
bun install --frozen-lockfile
|
|
38
38
|
npm install -g . --omit=optional --no-audit --no-fund
|
|
39
39
|
```
|
|
40
40
|
|
package/docs/wiki/Quick-Start.md
CHANGED
|
@@ -7,7 +7,7 @@ Until the npm RC is publicly available, use the verified source path:
|
|
|
7
7
|
```bash
|
|
8
8
|
git clone https://github.com/moretea-labs/forge.git
|
|
9
9
|
cd forge
|
|
10
|
-
|
|
10
|
+
bun install --frozen-lockfile
|
|
11
11
|
npm install -g . --omit=optional --no-audit --no-fund
|
|
12
12
|
```
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moretea-labs/forge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Local-first action assistant for software work with resumable execution, bounded tools, and reviewable evidence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Moretea Labs contributors",
|
|
@@ -72,9 +72,22 @@ grep -q "$README_CHANNEL_EN" README.md || { echo "[public-docs] English README m
|
|
|
72
72
|
grep -q "$README_CHANNEL_ZH" README.zh-CN.md || { echo "[public-docs] Chinese README must state the npm channel for $PACKAGE_VERSION" >&2; exit 1; }
|
|
73
73
|
grep -q 'See \[README.md\](README.md)' README.en.md || { echo "[public-docs] README.en.md must point to the maintained English README" >&2; exit 1; }
|
|
74
74
|
|
|
75
|
+
source_install_docs=(
|
|
76
|
+
README.md README.zh-CN.md
|
|
77
|
+
docs/tutorials/01-install-and-start.md docs/tutorials/01-install-and-start.zh-CN.md
|
|
78
|
+
docs/wiki/Quick-Start.md docs/wiki/Installation.md
|
|
79
|
+
)
|
|
80
|
+
for path in "${source_install_docs[@]}"; do
|
|
81
|
+
grep -q 'bun install --frozen-lockfile' "$path" || { echo "[public-docs] source checkout must use the CI-authoritative Bun frozen lock: $path" >&2; exit 1; }
|
|
82
|
+
grep -q 'npm install -g \.' "$path" || { echo "[public-docs] missing source package install: $path" >&2; exit 1; }
|
|
83
|
+
done
|
|
84
|
+
if git grep -n 'npm ci --ignore-scripts --no-audit --no-fund' -- "${source_install_docs[@]}" >/dev/null; then
|
|
85
|
+
echo "[public-docs] generic npm ci must not be advertised for Forge source checkouts" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
75
89
|
for path in docs/tutorials/01-install-and-start.md docs/tutorials/01-install-and-start.zh-CN.md; do
|
|
76
90
|
grep -q 'Node.js 20.10' "$path" || { echo "[public-docs] missing Node baseline: $path" >&2; exit 1; }
|
|
77
|
-
grep -q 'npm install -g \.' "$path" || { echo "[public-docs] missing source install: $path" >&2; exit 1; }
|
|
78
91
|
grep -q "$INSTALL_SPEC" "$path" || { echo "[public-docs] missing package command for $PACKAGE_VERSION: $path" >&2; exit 1; }
|
|
79
92
|
done
|
|
80
93
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join, resolve } from 'path';
|
|
4
|
+
import { resolveRepoPreferredControllerHome } from '../src/cli/repositories/controller-home';
|
|
5
|
+
import { listRepositories } from '../src/cli/repositories/registry';
|
|
6
|
+
import { migrateChatgptAutomationSchedule } from '../src/runtime/workflow/schedules/chatgpt-automation-migration';
|
|
7
|
+
import { listSchedules, saveSchedule } from '../src/runtime/workflow/schedules/store';
|
|
8
|
+
import type { RepositorySchedule } from '../src/runtime/workflow/schedules/types';
|
|
9
|
+
|
|
10
|
+
interface Args { controllerHome?: string; apply: boolean; json: boolean; }
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv: string[]): Args {
|
|
13
|
+
const args: Args = { apply: false, json: false };
|
|
14
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
15
|
+
const value = argv[index];
|
|
16
|
+
if (value === '--apply') args.apply = true;
|
|
17
|
+
else if (value === '--json') args.json = true;
|
|
18
|
+
else if (value === '--controller-home') args.controllerHome = resolve(argv[++index] ?? '');
|
|
19
|
+
else throw new Error(`unknown argument: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function main(): void {
|
|
25
|
+
const args = parseArgs(process.argv.slice(2));
|
|
26
|
+
const controllerHome = resolveRepoPreferredControllerHome(process.cwd(), args.controllerHome);
|
|
27
|
+
const repositories = listRepositories(controllerHome, { includeRemoved: true });
|
|
28
|
+
const candidates: Array<{ repositoryName: string; original: RepositorySchedule; migrated: RepositorySchedule }> = [];
|
|
29
|
+
for (const repository of repositories) {
|
|
30
|
+
for (const schedule of listSchedules(controllerHome, repository.repoId)) {
|
|
31
|
+
const result = migrateChatgptAutomationSchedule(schedule);
|
|
32
|
+
if (result.changed) candidates.push({ repositoryName: repository.displayName, original: schedule, migrated: result.schedule });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let backupPath: string | undefined;
|
|
37
|
+
if (args.apply && candidates.length > 0) {
|
|
38
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
39
|
+
backupPath = join(controllerHome, 'backups', `chatgpt-automation-schedules-${stamp}.json`);
|
|
40
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
41
|
+
writeFileSync(backupPath, `${JSON.stringify({ schemaVersion: 1, createdAt: new Date().toISOString(), schedules: candidates.map((item) => item.original) }, null, 2)}\n`);
|
|
42
|
+
for (const candidate of candidates) saveSchedule(controllerHome, candidate.migrated);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const output = {
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
controllerHome,
|
|
48
|
+
repositoryCount: repositories.length,
|
|
49
|
+
migratedCount: candidates.length,
|
|
50
|
+
writesApplied: args.apply,
|
|
51
|
+
backupPath,
|
|
52
|
+
schedules: candidates.map((item) => ({ repoId: item.original.repoId, repositoryName: item.repositoryName, scheduleId: item.original.scheduleId, name: item.original.name })),
|
|
53
|
+
};
|
|
54
|
+
if (args.json) console.log(JSON.stringify(output, null, 2));
|
|
55
|
+
else {
|
|
56
|
+
console.log(`[chatgpt-automation-migration] ${candidates.length} schedule(s) eligible across ${repositories.length} repository record(s).`);
|
|
57
|
+
console.log(args.apply ? `[chatgpt-automation-migration] Applied. Backup: ${backupPath ?? 'not needed'}` : '[chatgpt-automation-migration] Preview only; pass --apply after the new Runtime baseline is active.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try { main(); } catch (error) {
|
|
62
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
@@ -21,7 +21,7 @@ Usage:
|
|
|
21
21
|
|
|
22
22
|
Program-level sprint backlog helper. PRDs live in plans/prds/ as the upper
|
|
23
23
|
planning layer; sprints live in plans/sprints/ as ordered execution backlogs.
|
|
24
|
-
Each backlog row is expanded with
|
|
24
|
+
Each backlog row is expanded with /plan before the existing plan -> contract
|
|
25
25
|
-> worktree flow. tasks/todos.md stays the deferred-goal ledger.
|
|
26
26
|
|
|
27
27
|
start-task reserves the next (or named) pending backlog row and can capture a
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
} from '../chatgpt-browser/engine';
|
|
14
14
|
import type { BrowserProviderName, BrowserSessionStatus, NativeBrowserChannel, ThinkingLevel } from '../chatgpt-browser/types';
|
|
15
15
|
import { durableControllerHome } from '../repositories/controller-home';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
runWorkChatgptContinuation,
|
|
18
|
+
type ChatgptAutomationReasoning,
|
|
19
|
+
type ChatgptAutomationTabPolicy,
|
|
20
|
+
} from '../../runtime/control-plane/launcher/chatgpt-work-continuation';
|
|
17
21
|
|
|
18
22
|
interface BrowserCommonOptions {
|
|
19
23
|
repo?: string;
|
|
@@ -84,6 +88,9 @@ interface WorkChatgptContinueOptions extends BrowserCommonOptions {
|
|
|
84
88
|
title?: string;
|
|
85
89
|
session?: string;
|
|
86
90
|
conversationUrl?: string;
|
|
91
|
+
model?: string;
|
|
92
|
+
reasoning?: string;
|
|
93
|
+
tabPolicy?: string;
|
|
87
94
|
timeoutMs?: string;
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -109,6 +116,18 @@ interface BrowserFollowupOptions extends BrowserCommonOptions {
|
|
|
109
116
|
oracleBin?: string;
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
function parseAutomationReasoning(value?: string): ChatgptAutomationReasoning | undefined {
|
|
120
|
+
if (value === undefined) return undefined;
|
|
121
|
+
if (value === 'medium' || value === 'high' || value === 'xhigh') return value;
|
|
122
|
+
throw new Error(`invalid --reasoning "${value}" (expected: medium, high, xhigh)`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseAutomationTabPolicy(value?: string): ChatgptAutomationTabPolicy | undefined {
|
|
126
|
+
if (value === undefined) return undefined;
|
|
127
|
+
if (value === 'auto' || value === 'reuse' || value === 'new') return value;
|
|
128
|
+
throw new Error(`invalid --tab-policy "${value}" (expected: auto, reuse, new)`);
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
function parseProvider(value?: string): BrowserProviderName {
|
|
113
132
|
if (value === undefined || value === 'oracle') return 'oracle';
|
|
114
133
|
if (value === 'native') return 'native';
|
|
@@ -384,8 +403,11 @@ export function buildChatgptCommand(): Command {
|
|
|
384
403
|
.requiredOption('--work-id <work-id>', 'Stable Forge Work id')
|
|
385
404
|
.requiredOption('--prompt <text>', 'Bounded continuation prompt')
|
|
386
405
|
.option('--title <title>', 'Human-readable local conversation alias')
|
|
387
|
-
.option('--session <session-id>', '
|
|
406
|
+
.option('--session <session-id>', 'Saved browser session to seed or resume the Work binding')
|
|
388
407
|
.option('--conversation-url <url>', 'Explicit ChatGPT conversation URL to seed the binding')
|
|
408
|
+
.option('--model <label>', 'Requested ChatGPT automation model', 'gpt-5.6')
|
|
409
|
+
.option('--reasoning <level>', 'Reasoning level: medium|high|xhigh', 'high')
|
|
410
|
+
.option('--tab-policy <policy>', 'Browser tab policy: auto|reuse|new', 'auto')
|
|
389
411
|
.option('--timeout-ms <ms>', 'Assistant timeout in milliseconds')
|
|
390
412
|
.action((rawOpts: WorkChatgptContinueOptions) => {
|
|
391
413
|
void runChatgptAction(async () => {
|
|
@@ -398,6 +420,9 @@ export function buildChatgptCommand(): Command {
|
|
|
398
420
|
title: rawOpts.title,
|
|
399
421
|
browserSessionId: rawOpts.session,
|
|
400
422
|
conversationUrl: rawOpts.conversationUrl,
|
|
423
|
+
model: rawOpts.model,
|
|
424
|
+
reasoning: parseAutomationReasoning(rawOpts.reasoning),
|
|
425
|
+
tabPolicy: parseAutomationTabPolicy(rawOpts.tabPolicy),
|
|
401
426
|
timeoutMs: parsePositiveInteger('timeout-ms', rawOpts.timeoutMs),
|
|
402
427
|
});
|
|
403
428
|
console.log(JSON.stringify({
|
|
@@ -408,6 +433,10 @@ export function buildChatgptCommand(): Command {
|
|
|
408
433
|
conversationId: result.conversationId,
|
|
409
434
|
localAlias: result.localAlias,
|
|
410
435
|
resumedFromBinding: result.resumedFromBinding,
|
|
436
|
+
model: result.model,
|
|
437
|
+
reasoning: result.reasoning,
|
|
438
|
+
tabPolicy: result.tabPolicy,
|
|
439
|
+
executionPreferenceVerified: result.executionPreferenceVerified,
|
|
411
440
|
error: result.error,
|
|
412
441
|
}, null, 2));
|
|
413
442
|
});
|
|
@@ -5,11 +5,11 @@ import { dirname, isAbsolute, join, resolve, sep } from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
import { resolveControllerHome, controllerSystemRoot } from '../repositories/controller-home';
|
|
8
|
-
import { installExternalPluginRegistration, listExternalPluginRegistrations, type ExternalPluginRegistrationInput } from '../../runtime/plugins/external-registration';
|
|
8
|
+
import { installExternalPluginRegistration, listExternalPluginRegistrations, type ExternalPluginRegistration, type ExternalPluginRegistrationInput } from '../../runtime/plugins/external-registration';
|
|
9
9
|
import { createDesktopOperatorRegistrationInput } from '../../runtime/plugins/desktop-operator-registration';
|
|
10
10
|
import { controllerPluginRepository, getAssistantPluginManifest, syncAssistantPluginRegistry } from '../../runtime/plugins/store';
|
|
11
11
|
|
|
12
|
-
interface RegistryEntry { id:string; name:string; version:string; description:string; repository:string; ref:string; installer:string; platforms:NodeJS.Platform[]; }
|
|
12
|
+
export interface RegistryEntry { id:string; name:string; version:string; description:string; repository:string; ref:string; installer:string; providerVersion?:string; protocolVersion?:string; platforms:NodeJS.Platform[]; }
|
|
13
13
|
interface Registry { schemaVersion:1; plugins:RegistryEntry[]; }
|
|
14
14
|
interface CliOptions { json?:boolean; controllerHome?:string; refresh?:boolean; }
|
|
15
15
|
const packageRoot=resolve(dirname(fileURLToPath(import.meta.url)),'../../..');
|
|
@@ -24,11 +24,26 @@ function registry():Registry{
|
|
|
24
24
|
if(!plugin.repository.startsWith('https://github.com/moretea-labs/')||!plugin.repository.endsWith('.git'))throw new Error(`PLUGIN_CATALOG_UNTRUSTED_REPOSITORY: ${plugin.id}`);
|
|
25
25
|
if(!/^v\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(plugin.ref))throw new Error(`PLUGIN_CATALOG_UNPINNED_REF: ${plugin.id}`);
|
|
26
26
|
if(plugin.installer!=='forge-plugin-install.mjs')throw new Error(`PLUGIN_CATALOG_INSTALLER_INVALID: ${plugin.id}`);
|
|
27
|
+
if(plugin.providerVersion!==undefined&&!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(plugin.providerVersion))throw new Error(`PLUGIN_CATALOG_PROVIDER_VERSION_INVALID: ${plugin.id}`);
|
|
28
|
+
if(plugin.protocolVersion!==undefined&&!/^\d+\.\d+$/.test(plugin.protocolVersion))throw new Error(`PLUGIN_CATALOG_PROTOCOL_VERSION_INVALID: ${plugin.id}`);
|
|
27
29
|
ids.add(plugin.id);
|
|
28
30
|
}
|
|
29
31
|
return value;
|
|
30
32
|
}
|
|
31
33
|
function within(root:string,relative:string):string{const target=resolve(root,relative);const base=`${resolve(root)}${sep}`;if(target!==resolve(root)&&!target.startsWith(base))throw new Error('PLUGIN_INSTALL_PATH_ESCAPE');return target;}
|
|
34
|
+
export function pluginCatalogCompatibility(entry:RegistryEntry,platform:NodeJS.Platform=process.platform):{compatible:boolean;reason?:string}{
|
|
35
|
+
if(!entry.platforms.includes(platform))return{compatible:false,reason:`unsupported platform: ${platform}`};
|
|
36
|
+
if(entry.providerVersion&&entry.providerVersion!==entry.version)return{compatible:false,reason:`catalog version ${entry.version} does not match pinned provider version ${entry.providerVersion}`};
|
|
37
|
+
return{compatible:true};
|
|
38
|
+
}
|
|
39
|
+
export function officialPluginCatalogItems(platform:NodeJS.Platform=process.platform){return registry().plugins.map(entry=>({...entry,...pluginCatalogCompatibility(entry,platform)}));}
|
|
40
|
+
export function externalPluginListItem(controllerHome:string,registration:ExternalPluginRegistration){
|
|
41
|
+
const base={pluginId:registration.pluginId,version:registration.pluginVersion,provider:registration.provider,enabled:registration.enabled,scope:registration.scope,transport:registration.transport.kind};
|
|
42
|
+
if(registration.scope==='repository')return{...base,healthScope:'repository_context_required' as const};
|
|
43
|
+
const repository=controllerPluginRepository(controllerHome);
|
|
44
|
+
try{return{...base,health:getAssistantPluginManifest(controllerHome,repository,registration.pluginId).health};}
|
|
45
|
+
catch(error){return{...base,health:{state:'error',message:error instanceof Error?error.message:String(error)}};}
|
|
46
|
+
}
|
|
32
47
|
function run(command:string,args:string[],cwd:string,timeoutMs=120000):string{
|
|
33
48
|
const result=spawnSync(command,args,{cwd,encoding:'utf8',timeout:timeoutMs,maxBuffer:32*1024*1024,shell:false,windowsHide:true});
|
|
34
49
|
if(result.error)throw new Error(`PLUGIN_INSTALL_COMMAND_FAILED: ${result.error.message}`);
|
|
@@ -40,17 +55,29 @@ function installerJson(stdout:string):Record<string,unknown>{
|
|
|
40
55
|
for(let i=lines.length-1;i>=0;i--){try{const x=JSON.parse(lines[i]);if(x&&typeof x==='object'&&!Array.isArray(x)&&x.schemaVersion===1)return x;}catch{}}
|
|
41
56
|
throw new Error('PLUGIN_INSTALLER_OUTPUT_INVALID');
|
|
42
57
|
}
|
|
58
|
+
export function installerNextSteps(result:Record<string,unknown>):string[]{
|
|
59
|
+
if(!Array.isArray(result.nextSteps))return[];
|
|
60
|
+
return result.nextSteps
|
|
61
|
+
.filter((value):value is string=>typeof value==='string')
|
|
62
|
+
.map(value=>value.replace(/[\u0000-\u001F\u007F]/g,' ').trim().slice(0,1000))
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.slice(0,10);
|
|
65
|
+
}
|
|
43
66
|
function registrationFrom(result:Record<string,unknown>,entry:RegistryEntry):ExternalPluginRegistrationInput{
|
|
44
67
|
if(result.registration){const input=result.registration as ExternalPluginRegistrationInput;if(input.pluginId!==entry.id)throw new Error('PLUGIN_INSTALLER_ID_MISMATCH');if(input.pluginVersion!==entry.version)throw new Error('PLUGIN_INSTALLER_VERSION_MISMATCH');return input;}
|
|
45
68
|
const facts=result.providerInstall as Record<string,unknown>|undefined;
|
|
46
69
|
if(!facts||facts.kind!=='desktop_operator'||entry.id!=='desktop_operator')throw new Error('PLUGIN_INSTALLER_REGISTRATION_MISSING');
|
|
47
|
-
|
|
70
|
+
const expectedProviderVersion=entry.providerVersion??entry.version;
|
|
71
|
+
const expectedProtocolVersion=entry.protocolVersion??'1.0';
|
|
72
|
+
if(facts.pluginVersion!==expectedProviderVersion)throw new Error(`PLUGIN_INSTALLER_PROVIDER_VERSION_MISMATCH: expected ${expectedProviderVersion} for ${entry.id}@${entry.version}, actual ${String(facts.pluginVersion??'missing')}`);
|
|
73
|
+
if(facts.protocolVersion!==expectedProtocolVersion)throw new Error(`PLUGIN_INSTALLER_PROTOCOL_VERSION_MISMATCH: expected ${expectedProtocolVersion} for ${entry.id}@${entry.version}, actual ${String(facts.protocolVersion??'missing')}`);
|
|
48
74
|
const socketPath=typeof facts.socketPath==='string'?facts.socketPath:'';const launchAgentLabel=typeof facts.launchAgentLabel==='string'?facts.launchAgentLabel:'';const expectedProgramContains=typeof facts.expectedProgramContains==='string'?facts.expectedProgramContains:'';
|
|
49
75
|
if(!isAbsolute(socketPath)||!launchAgentLabel||!expectedProgramContains)throw new Error('PLUGIN_INSTALLER_PROVIDER_FACTS_INVALID');
|
|
50
|
-
return createDesktopOperatorRegistrationInput({socketPath,launchAgentLabel,expectedProgramContains,pluginVersion:
|
|
76
|
+
return createDesktopOperatorRegistrationInput({socketPath,launchAgentLabel,expectedProgramContains,pluginVersion:expectedProviderVersion,protocolVersion:expectedProtocolVersion});
|
|
51
77
|
}
|
|
52
78
|
function install(entry:RegistryEntry,controllerHome:string):Record<string,unknown>{
|
|
53
|
-
|
|
79
|
+
const compatibility=pluginCatalogCompatibility(entry,process.platform);
|
|
80
|
+
if(!compatibility.compatible)throw new Error(`PLUGIN_CATALOG_INCOMPATIBLE: ${entry.id}@${entry.version}: ${compatibility.reason}`);
|
|
54
81
|
const packagesRoot=join(controllerSystemRoot(controllerHome),'plugins','packages');mkdirSync(packagesRoot,{recursive:true,mode:0o700});
|
|
55
82
|
const finalRoot=join(packagesRoot,entry.id),stage=join(packagesRoot,`.${entry.id}.${randomUUID()}.staging`),backup=join(packagesRoot,`.${entry.id}.${randomUUID()}.backup`);let backed=false;
|
|
56
83
|
try{
|
|
@@ -62,13 +89,13 @@ function install(entry:RegistryEntry,controllerHome:string):Record<string,unknow
|
|
|
62
89
|
const installed=installExternalPluginRegistration(controllerHome,registrationFrom(result,entry));
|
|
63
90
|
const repository=controllerPluginRepository(controllerHome);syncAssistantPluginRegistry(controllerHome,repository);const plugin=getAssistantPluginManifest(controllerHome,repository,entry.id);
|
|
64
91
|
if(backed)rmSync(backup,{recursive:true,force:true});
|
|
65
|
-
return{pluginId:entry.id,version:entry.version,packageRoot:finalRoot,registrationRevision:installed.revision,enabled:plugin.enabled,health:plugin.health};
|
|
92
|
+
return{pluginId:entry.id,version:entry.version,packageRoot:finalRoot,registrationRevision:installed.revision,enabled:plugin.enabled,health:plugin.health,nextSteps:installerNextSteps(result)};
|
|
66
93
|
}catch(error){rmSync(stage,{recursive:true,force:true});if(backed&&existsSync(finalRoot))rmSync(finalRoot,{recursive:true,force:true});if(backed&&existsSync(backup))renameSync(backup,finalRoot);throw error;}
|
|
67
94
|
}
|
|
68
95
|
export function buildPluginCommand():Command{
|
|
69
96
|
const root=new Command('plugin').description('Discover and install trusted Forge plugins');
|
|
70
|
-
root.command('catalog').description('List official plugins').option('--json','Output JSON').action((o:CliOptions)=>{const items=
|
|
71
|
-
root.command('list').description('List installed external plugins').option('--json','Output JSON').option('--controller-home <path>','Override Controller Home').option('--refresh','Probe providers').action((o:CliOptions)=>{const home=resolveControllerHome(o.controllerHome),repo=controllerPluginRepository(home);if(o.refresh)syncAssistantPluginRegistry(home,repo);const items=listExternalPluginRegistrations(home).map(r=>
|
|
72
|
-
root.command('install <plugin-id>').description('Install/update an official plugin from a pinned public release').option('--json','Output JSON').option('--controller-home <path>','Override Controller Home').action((id:string,o:CliOptions)=>{const entry=registry().plugins.find(x=>x.id===id);if(!entry)throw new Error(`PLUGIN_NOT_IN_OFFICIAL_CATALOG: ${id}`);const result=install(entry,resolveControllerHome(o.controllerHome));if(o.json)console.log(JSON.stringify(result,null,2));else
|
|
97
|
+
root.command('catalog').description('List official plugins').option('--json','Output JSON').action((o:CliOptions)=>{const items=officialPluginCatalogItems();if(o.json)console.log(JSON.stringify({schemaVersion:1,platform:process.platform,plugins:items},null,2));else items.forEach(x=>console.log(`${x.id}\t${x.version}\t${x.compatible?'compatible':`incompatible (${x.reason})`}\t${x.name}`));});
|
|
98
|
+
root.command('list').description('List installed external plugins').option('--json','Output JSON').option('--controller-home <path>','Override Controller Home').option('--refresh','Probe controller-scoped providers').action((o:CliOptions)=>{const home=resolveControllerHome(o.controllerHome),repo=controllerPluginRepository(home);if(o.refresh)syncAssistantPluginRegistry(home,repo);const items=listExternalPluginRegistrations(home).map(r=>externalPluginListItem(home,r));if(o.json)console.log(JSON.stringify({schemaVersion:1,plugins:items},null,2));else if(!items.length)console.log('No external Forge plugins installed.');else items.forEach(x=>console.log(`${x.pluginId}\t${x.version}\t${x.enabled?'enabled':'disabled'}\t${'healthScope' in x?'repository-context-required':((x.health as {state?:string})?.state??'unknown')}`));});
|
|
99
|
+
root.command('install <plugin-id>').description('Install/update an official plugin from a pinned public release').option('--json','Output JSON').option('--controller-home <path>','Override Controller Home').action((id:string,o:CliOptions)=>{const entry=registry().plugins.find(x=>x.id===id);if(!entry)throw new Error(`PLUGIN_NOT_IN_OFFICIAL_CATALOG: ${id}`);const result=install(entry,resolveControllerHome(o.controllerHome));if(o.json)console.log(JSON.stringify(result,null,2));else{console.log(`Installed ${entry.name} ${entry.version} (${(result.health as {state?:string})?.state??'unknown'}).`);for(const step of (result.nextSteps as string[]|undefined)??[])console.log(`- ${step}`);}});
|
|
73
100
|
return root;
|
|
74
101
|
}
|