carthagent 1.1.0-beta.1 → 1.1.0-beta.3
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/docs/beta2-published-acceptance.md +79 -0
- package/docs/cloud-onboarding-acceptance.md +68 -0
- package/docs/release-notes-beta.2.md +60 -0
- package/docs/release-notes-beta.3.md +47 -0
- package/lib/cloud/client.mjs +13 -7
- package/lib/opencode/cli-runtime.mjs +56 -18
- package/lib/opencode/cli.mjs +10 -6
- package/lib/opencode/provider-policy.mjs +1 -1
- package/lib/opencode/runtime-release.json +11 -11
- package/lib/providers/names.mjs +1 -1
- package/lib/tui/models.mjs +1 -1
- package/package.json +1 -1
- package/plugins/carthagent/src/rpc.ts +2 -2
- package/plugins/carthagent/src/server/providers.ts +10 -9
- package/plugins/carthagent/src/server/ship-rpc.ts +12 -10
- package/plugins/carthagent/src/tui.tsx +18 -8
- package/plugins/carthagent/src/ui/client.ts +1 -1
- package/plugins/carthagent/src/ui/commands.ts +2 -2
- package/plugins/carthagent/src/ui/daily-quests.tsx +1 -1
- package/plugins/carthagent/src/ui/voyages.tsx +1 -1
- package/tests/cli.test.mjs +2 -2
- package/tests/cloud-client.test.mjs +2 -2
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -1
- package/tests/native-provider-policy.test.mjs +4 -2
- package/tests/native-ship-rpc.test.mjs +14 -2
- package/tests/opencode-cli.test.mjs +38 -4
- package/tests/opencode-providers.test.mjs +4 -3
- package/tests/opencode-ship-rpc.test.mjs +2 -2
- package/tests/tui.test.mjs +2 -2
- package/tools/native-fixture-stop-diagnostics.mjs +3 -1
- package/tools/native-terminal-semantics.py +7 -1
- package/tools/test-native-host.mjs +4 -2
- package/tools/test-native-ship-models.mjs +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Beta.2 release acceptance
|
|
2
|
+
|
|
3
|
+
November 2, 2026. Immutable tag `v1.1.0-beta.2` points to
|
|
4
|
+
`e14aeb17c213a927a27e9522da65db5928219647`.
|
|
5
|
+
|
|
6
|
+
## Completed
|
|
7
|
+
|
|
8
|
+
- Native onboarding PR #238 and first-task guidance PR #239 merged. Release PR
|
|
9
|
+
#240 merged with tag ancestry preserved at `fc5382364f41b1563ed137ed0db0c5711e041a83`.
|
|
10
|
+
- Five-platform prebuild [41505735348](https://github.com/B-icy/Carthagent/actions/runs/41505735348)
|
|
11
|
+
passed. Local manifest regeneration verified all archive/executable hashes and
|
|
12
|
+
embedded provenance against the pinned runtime recipe.
|
|
13
|
+
- Release CI [41509102387](https://github.com/B-icy/Carthagent/actions/runs/41509102387)
|
|
14
|
+
passed across Linux, macOS and Windows, including installed-package acceptance.
|
|
15
|
+
- Final draft acceptance [41511681097](https://github.com/B-icy/Carthagent/actions/runs/41511681097)
|
|
16
|
+
passed against the exact tag, assets and main ancestry.
|
|
17
|
+
- GitHub prerelease is public with five runtime archives, the manifest and
|
|
18
|
+
combined checksums. Independent anonymous verification of all five public
|
|
19
|
+
archives passed.
|
|
20
|
+
- Release workflow [41512736590](https://github.com/B-icy/Carthagent/actions/runs/41512736590)
|
|
21
|
+
passed anonymous installed-package acceptance and reports successful npm
|
|
22
|
+
publication with signed provenance (transparency log index 3648230788).
|
|
23
|
+
- Backend onboarding #178 and retention reporting #179 are deployed. Live
|
|
24
|
+
authenticated account identity and read-only plan terms were verified earlier.
|
|
25
|
+
|
|
26
|
+
## npm publication verified
|
|
27
|
+
|
|
28
|
+
After the workflow reported `+ carthagent@1.1.0-beta.2`, public registry requests
|
|
29
|
+
from the acceptance environment still returned 404 for that exact version.
|
|
30
|
+
Both npm CLI and cache-bypassed package metadata initially reported
|
|
31
|
+
`next=1.1.0-beta.1`, `latest=1.0.7`. Registry visibility subsequently converged;
|
|
32
|
+
no repeat publication was attempted.
|
|
33
|
+
|
|
34
|
+
The public beta.2 tarball's SHA-512 matches registry integrity and the provenance
|
|
35
|
+
subject digest. Sigstore verification passed for the GitHub Actions issuer and
|
|
36
|
+
the repository's `release.yml@refs/heads/main` identity. The statement identifies
|
|
37
|
+
release workflow 41512736590, and package `gitHead` matches the immutable tag.
|
|
38
|
+
Verified tags: `next=1.1.0-beta.2`, `latest=1.0.7`.
|
|
39
|
+
Evidence: `/tmp/opencode/beta2-npm-acceptance.json`.
|
|
40
|
+
|
|
41
|
+
## Installed update verified
|
|
42
|
+
|
|
43
|
+
The website command `npm install -g carthagent@next` installed beta.2 into an
|
|
44
|
+
isolated Linux x64 home/prefix. Default runtime acquisition resolved the pinned
|
|
45
|
+
CTG-owned executable with SHA-256
|
|
46
|
+
`8a34735274c0b62049f6ae882dc955016fde54458efc18822fbcedb2e8eddeda`.
|
|
47
|
+
The beta update check reported the installed version as current.
|
|
48
|
+
Evidence: `/tmp/opencode/beta2-clean-install-acceptance.json`.
|
|
49
|
+
|
|
50
|
+
An isolated npm installation of beta.1 detected beta.2 with
|
|
51
|
+
`ctg update --channel beta --check`. Running `ctg update --channel beta` upgraded
|
|
52
|
+
it to beta.2; a profile sentinel remained byte-for-byte intact. This used a
|
|
53
|
+
disposable home and global npm prefix, not the user's normal installation.
|
|
54
|
+
Evidence: `/tmp/opencode/beta2-update-acceptance.json`.
|
|
55
|
+
|
|
56
|
+
## Remaining acceptance
|
|
57
|
+
|
|
58
|
+
Stripe sandbox access subsequently became available. Real purchases, upgrades,
|
|
59
|
+
SCA, renewal, failed renewal and cancellation were exercised separately against
|
|
60
|
+
the isolated backend (see backend `docs/stripe-sandbox-acceptance.md`).
|
|
61
|
+
|
|
62
|
+
An isolated published beta.2 login connected the authorized existing Cloud account.
|
|
63
|
+
A real range-fix task passed four independent tests. Continuing that same session
|
|
64
|
+
hit its persistent elapsed-time budget; beta.2 showed only an unexplained interrupt.
|
|
65
|
+
The follow-up source fix adds actionable budget diagnostics and explicit
|
|
66
|
+
`--reset-budget` support. With that source launcher and the same installed runtime,
|
|
67
|
+
profile and session, continuation added an ascending-across-zero regression and
|
|
68
|
+
passed all five independent tests. This launcher fix is not part of immutable beta.2.
|
|
69
|
+
Execution budgets are not billing credit and never reset on payment or ordinary
|
|
70
|
+
continuation. Evidence: `/tmp/opencode/stripe-acceptance/live-continuation-fixed.log`.
|
|
71
|
+
|
|
72
|
+
Full task continuation across a genuine payment boundary remains incomplete:
|
|
73
|
+
the real model task used existing Cloud credit and sandbox payments used a separate
|
|
74
|
+
backend without a model upstream. Screenshot approval is not claimed. Local terminal timing failures
|
|
75
|
+
were followed by passing full local, CI and draft acceptance; this does not prove
|
|
76
|
+
that every terminal timing issue is resolved.
|
|
77
|
+
|
|
78
|
+
The normal installed profile and Invest-With-Friends files were not changed by
|
|
79
|
+
these release checks. Stable npm `latest` remains 1.0.7.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Cloud onboarding — beta.2 implementation
|
|
2
|
+
|
|
3
|
+
November 2, 2026. Beta.2 is published on GitHub and npm `next`; `latest` remains
|
|
4
|
+
1.0.7. See [published acceptance](beta2-published-acceptance.md) for final CI,
|
|
5
|
+
draft/public runtime gates, signed npm provenance, Linux clean installation and
|
|
6
|
+
isolated beta.1 → beta.2 update evidence. Historical preparation notes below do
|
|
7
|
+
not supersede that release record.
|
|
8
|
+
|
|
9
|
+
Native PR #238 merged at `a5ad2164d056fee7f7183579ce6cc97305bae53b` after CI
|
|
10
|
+
41504674623 passed. First-task guidance is tracked in PR #239. Beta.2 runtime
|
|
11
|
+
prebuild 41505735348 succeeded. All five downloaded archives, executable digests
|
|
12
|
+
and pinned provenance were independently verified by regenerating the candidate
|
|
13
|
+
manifest locally. Package and lockfile target beta.2 and pin that exact manifest;
|
|
14
|
+
publication remains pending.
|
|
15
|
+
|
|
16
|
+
## Changes
|
|
17
|
+
|
|
18
|
+
- Use Carthagent Cloud for hosted models; retain Ship mode, internal IDs and
|
|
19
|
+
compatibility login aliases.
|
|
20
|
+
- Fresh device sign-in has one primary flow. Legacy import and upstream key
|
|
21
|
+
configuration require `CARTHAGENT_ADVANCED_AUTH=1`.
|
|
22
|
+
- Billing selects Builder/Pro/Max and retrieves authenticated server quote terms.
|
|
23
|
+
TUI review precedes opening checkout. Existing subscribers review exact Stripe
|
|
24
|
+
proration in the browser before confirming a change.
|
|
25
|
+
- `/billing refresh` and `ctg billing refresh` show current account access and
|
|
26
|
+
require explicit continuation in the existing task session.
|
|
27
|
+
- Cloud account text identifies the signed-in email; usage recovery distinguishes
|
|
28
|
+
starter credit, daily/monthly limits, billing and own-provider access.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
- Full native suite: 1,131 passed, 5 skipped, 0 failed.
|
|
33
|
+
`/tmp/opencode/onboarding-native-reviewed.log`
|
|
34
|
+
- Native build and typecheck pass.
|
|
35
|
+
`/tmp/opencode/onboarding-native-reviewed-build.log`
|
|
36
|
+
- RPC regression verifies that eligibility comes from the authenticated server
|
|
37
|
+
and quoting does not create a checkout.
|
|
38
|
+
- Native host acceptance passes at `b715ff282b` after correcting stale fresh-auth
|
|
39
|
+
and Cloud-name expectations. Both CI installed-package jobs passed. Run
|
|
40
|
+
41502591162 passed Linux/macOS and Windows 24; Windows 22 hit the two-second
|
|
41
|
+
PowerShell diagnostics timeout, while its parallel run passed. Windows
|
|
42
|
+
diagnostics now allow bounded ten-second cold startup.
|
|
43
|
+
- Installed package acceptance and its focused terminal rerun reproduced a
|
|
44
|
+
reconnect overlay. DEBUG-enabled source and installed probes passed; a normal
|
|
45
|
+
source run subsequently hit its existing terminal-snapshot deadline. This is
|
|
46
|
+
timing-sensitive local acceptance, not proof of a fixed runtime defect. Both
|
|
47
|
+
earlier CI package checks passed, and corrected CI passed before merge.
|
|
48
|
+
- The subsequent full non-focused installed-package run passed all 28
|
|
49
|
+
probes/scenarios, including terminal semantics, using the package-pinned
|
|
50
|
+
beta.1 owned runtime (`/tmp/opencode/onboarding-native-package-final.log`).
|
|
51
|
+
This closes the local rerun gate for the onboarding source; beta.2 still
|
|
52
|
+
requires acceptance against its own final packaged manifest and release assets.
|
|
53
|
+
|
|
54
|
+
## Outstanding gates
|
|
55
|
+
|
|
56
|
+
Backend counterpart: `B-icy/Carthagent-Backend` PR #178 merged at
|
|
57
|
+
`b0eacdcbdb71be4c099b1b3839a11164b2d69232`; production deployment 41502378359
|
|
58
|
+
succeeded. Its `ONBOARDING_CONVERSION_AUDIT.md` tracks the full matrix. Live
|
|
59
|
+
authenticated identity and read-only Builder/Pro/Max handoff terms were verified.
|
|
60
|
+
Retention follow-up PR #179 also merged and deployment 41505976672 succeeded.
|
|
61
|
+
Stripe test access is unavailable. First-task/default-model guidance,
|
|
62
|
+
contextual exhaustion action, remaining legacy/docs naming and full measurement
|
|
63
|
+
segmentation still need work. No new end-to-end paid continuation or visual
|
|
64
|
+
screenshot acceptance is claimed.
|
|
65
|
+
|
|
66
|
+
The deployed website now precedes the native release. Publish a new immutable
|
|
67
|
+
beta and verify its exact public acquisition and upgrade. Preserve the user's
|
|
68
|
+
normal profile and Invest-With-Friends work during acceptance.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Carthagent 1.1.0-beta.2 — release candidate
|
|
2
|
+
|
|
3
|
+
This candidate simplifies Cloud sign-in and keeps billing recovery connected to
|
|
4
|
+
the task already open in your terminal. Publication and installed acceptance are
|
|
5
|
+
pending; these notes do not establish availability on npm.
|
|
6
|
+
|
|
7
|
+
## Cloud sign-in and first use
|
|
8
|
+
|
|
9
|
+
- Hosted models are named **Carthagent Cloud**. **Ship mode** remains the verified
|
|
10
|
+
coding workflow. Existing provider IDs and login aliases continue to work.
|
|
11
|
+
- Fresh Cloud sign-in goes directly through browser authorization. Legacy import
|
|
12
|
+
and upstream-key methods require `CARTHAGENT_ADVANCED_AUTH=1`.
|
|
13
|
+
- The browser identifies the account and terminal being connected. Device codes
|
|
14
|
+
last 20 minutes, with continuation-preserving sign-in recovery.
|
|
15
|
+
- Successful CLI sign-in suggests one small project task and points Cloud users
|
|
16
|
+
to `/account` for identity and actual available usage.
|
|
17
|
+
|
|
18
|
+
## Review billing, then continue explicitly
|
|
19
|
+
|
|
20
|
+
- `/billing checkout builder`, `pro` or `max` shows authenticated plan terms
|
|
21
|
+
before opening checkout. Eligible new Builder customers pay **$1 for the first
|
|
22
|
+
month, then $10/month**; Pro starts at **$25/month**, Max at **$50/month**.
|
|
23
|
+
- The separate one-time starter usage credit is attempted on first terminal
|
|
24
|
+
authorization. Browser signup alone does not grant it.
|
|
25
|
+
- Existing card subscribers review Stripe's prorated amount before confirming a
|
|
26
|
+
change. The renewal anchor and consumed/reserved usage are preserved. Existing
|
|
27
|
+
grandfathered subscriptions retain their documented allowance policy.
|
|
28
|
+
- After payment, run `/billing refresh` in the existing session, or
|
|
29
|
+
`ctg billing refresh` outside the TUI. Then explicitly continue the task.
|
|
30
|
+
Checkout does not automatically submit a prompt or restart work.
|
|
31
|
+
|
|
32
|
+
## Acceptance and limitations
|
|
33
|
+
|
|
34
|
+
Backend onboarding and retention reporting are deployed through PRs #178 and
|
|
35
|
+
#179. Native onboarding PR #238 is merged; first-task guidance is in PR #239.
|
|
36
|
+
The original onboarding suite passed 1,131 tests with five skips; corrected
|
|
37
|
+
native CI passed on Linux, macOS and Windows. These results precede final
|
|
38
|
+
beta.2 installed-package and public-acquisition acceptance.
|
|
39
|
+
|
|
40
|
+
Stripe test-mode access remains unavailable; real decline/SCA, tax/discount and
|
|
41
|
+
the full financial-transition matrix are not verified. No new live purchase or
|
|
42
|
+
end-to-end paid task continuation is claimed. Browser screenshot capture remains
|
|
43
|
+
unavailable. Local terminal acceptance initially showed event-stream timing
|
|
44
|
+
failures; the subsequent full installed-package run passed all 28 probes/scenarios
|
|
45
|
+
with the pinned beta.1 runtime, as did CI package acceptance. Final beta.2 draft
|
|
46
|
+
acceptance against its own manifest and assets is still required.
|
|
47
|
+
|
|
48
|
+
Verified-delivery rewards remain 25 coins, 100 XP and 1 km per eligible claimed
|
|
49
|
+
delivery, up to three per UTC day. Daily classifier assessments remain unrewarded.
|
|
50
|
+
|
|
51
|
+
## Planned release channel
|
|
52
|
+
|
|
53
|
+
The candidate targets npm `next` and the beta updater channel. Stable `latest`
|
|
54
|
+
remains 1.0.7. Exact installation and update instructions will be verified after
|
|
55
|
+
the immutable release and its five runtime archives are published.
|
|
56
|
+
|
|
57
|
+
Runtime prebuild: [41505735348](https://github.com/B-icy/Carthagent/actions/runs/41505735348),
|
|
58
|
+
source `d999644028ed0c0dc70bcaf58060db4018278e48`. The runtime recipe remains
|
|
59
|
+
combined-v5 on upstream `843a6c514e606b436a7fe713422ace0a93ad3d61`, compatibility
|
|
60
|
+
2.0.32. The final package must pin the generated archive and executable digests.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Carthagent 1.1.0-beta.3 — release candidate
|
|
2
|
+
|
|
3
|
+
This candidate makes continuation failures actionable. Publication is pending;
|
|
4
|
+
these notes do not establish npm availability.
|
|
5
|
+
|
|
6
|
+
## Continue the existing task
|
|
7
|
+
|
|
8
|
+
- An exhausted persistent execution budget now reports the reason and recovery
|
|
9
|
+
command instead of an unexplained interrupt.
|
|
10
|
+
- `--reset-budget` explicitly authorizes a fresh budget for a resumed session:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
ctg --print --continue --reset-budget "Continue the task"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Ordinary continuation and payment preserve the execution budget, including
|
|
17
|
+
elapsed idle time. Billing credit is separate. Interactive users can run
|
|
18
|
+
`/delivery-budget-reset` before continuing the existing task.
|
|
19
|
+
- CLI help consistently names Carthagent Cloud and documents `billing refresh`.
|
|
20
|
+
|
|
21
|
+
## Acceptance evidence
|
|
22
|
+
|
|
23
|
+
The source fix passed 1,132 tests with five skips, builds and lint. PR #242 CI
|
|
24
|
+
passed Linux, macOS and Windows, browser and installed-package gates. One macOS
|
|
25
|
+
22 service-stop identity timeout passed on retry; the separate push run passed.
|
|
26
|
+
|
|
27
|
+
The authorized existing Cloud account completed a real range-fix task. The same
|
|
28
|
+
session, profile and installed beta.2 runtime then continued with the source
|
|
29
|
+
launcher and explicit reset, adding a regression and passing all five project
|
|
30
|
+
tests independently. Final beta.3 installed acceptance remains required.
|
|
31
|
+
|
|
32
|
+
Backend PRs #180–181 are deployed: automatic payment-status checks, clearer
|
|
33
|
+
sign-in and refund guidance, direct Builder review, modern Stripe compatibility
|
|
34
|
+
and accurate scheduled-cancellation dates while retaining paid access.
|
|
35
|
+
Sandbox purchases, prorated upgrades, initial and upgrade SCA, declined-upgrade
|
|
36
|
+
recovery, renewal, failed renewal, automatic refunds and disputes were exercised
|
|
37
|
+
separately. These do not establish a single
|
|
38
|
+
useful-task → payment boundary → same-task continuation journey: the model task
|
|
39
|
+
used existing production credit and payments used an isolated sandbox backend.
|
|
40
|
+
|
|
41
|
+
## Release channel
|
|
42
|
+
|
|
43
|
+
Targets npm `next`; stable `latest` remains 1.0.7. Runtime prebuild
|
|
44
|
+
[41537249365](https://github.com/B-icy/Carthagent/actions/runs/41537249365) uses
|
|
45
|
+
reviewed source `c11103f8b57c6b3254473a78d535a404bea9a84a` and the unchanged
|
|
46
|
+
combined-v5 runtime recipe. Final assets and manifest must pass immutable draft
|
|
47
|
+
acceptance before publication.
|
package/lib/cloud/client.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export class CloudClient {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
async request(path, { method = 'GET', accessToken, body, signal } = {}) {
|
|
76
|
-
if (typeof this.fetch !== 'function') throw new Error('Carthagent
|
|
76
|
+
if (typeof this.fetch !== 'function') throw new Error('Carthagent Cloud requires fetch support.');
|
|
77
77
|
const headers = { accept: 'application/json' };
|
|
78
78
|
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
79
79
|
if (accessToken) headers.authorization = `Bearer ${accessToken}`;
|
|
@@ -128,6 +128,11 @@ export class CloudClient {
|
|
|
128
128
|
return this.request('/v1/billing/checkout', { method: 'POST', accessToken, body: { successUrl, cancelUrl, priceId }, signal });
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** @param {{ accessToken?: string, plan?: string, signal?: AbortSignal }} [input] */
|
|
132
|
+
quote({ accessToken, plan = 'builder', signal } = {}) {
|
|
133
|
+
return this.request(`/v1/billing/quote?plan=${encodeURIComponent(plan)}`, { accessToken, signal });
|
|
134
|
+
}
|
|
135
|
+
|
|
131
136
|
portal({ accessToken, returnUrl, signal } = {}) {
|
|
132
137
|
return this.request('/v1/billing/portal', { method: 'POST', accessToken, body: { returnUrl }, signal });
|
|
133
138
|
}
|
|
@@ -184,10 +189,10 @@ function formatEpochSeconds(value) {
|
|
|
184
189
|
|
|
185
190
|
export function cloudManagedUsageRecovery(value) {
|
|
186
191
|
const text = String(value?.code || value?.message || value || '');
|
|
187
|
-
if (/daily_limit_reached|managed daily usage limit/i.test(text)) return '
|
|
188
|
-
if (/monthly_allowance_exhausted|managed monthly allowance/i.test(text)) return '
|
|
189
|
-
if (/subscription_inactive|managed subscription is not active/i.test(text)) return '
|
|
190
|
-
if (/insufficient_quota|insufficient.credit|credit (?:is )?exhausted/i.test(text)) return 'Carthagent
|
|
192
|
+
if (/daily_limit_reached|managed daily usage limit/i.test(text)) return 'Cloud daily usage is exhausted. It resets at 00:00 UTC. Use /account for your plan and headroom, /billing portal to manage billing, or /login for your own provider (BYOK). Your task stays in this session.';
|
|
193
|
+
if (/monthly_allowance_exhausted|managed monthly allowance/i.test(text)) return 'Cloud monthly usage is exhausted. Use /account for your renewal date, /billing portal to manage billing, or /login for your own provider (BYOK). To review a different plan use /billing checkout pro or /billing checkout max. After payment use /billing refresh, then explicitly continue this task.';
|
|
194
|
+
if (/subscription_inactive|managed subscription is not active/i.test(text)) return 'Cloud access is awaiting payment or activation. Use /billing refresh to check, /billing portal to manage payment, or /login for your own provider.';
|
|
195
|
+
if (/insufficient_quota|insufficient.credit|credit (?:is )?exhausted/i.test(text)) return 'Carthagent Cloud usage credit is exhausted. Use /billing checkout builder to review your eligible price (new Builder customers: $1 first month, then $10/month), or /login for your own provider. After payment use /billing refresh, then explicitly continue this task.';
|
|
191
196
|
return null;
|
|
192
197
|
}
|
|
193
198
|
|
|
@@ -256,9 +261,10 @@ export function formatCloudAccount(account, { usage } = {}) {
|
|
|
256
261
|
const managed = account?.managedUsage;
|
|
257
262
|
const planName = managed?.planName || 'plan';
|
|
258
263
|
const lines = [
|
|
259
|
-
`Carthagent
|
|
264
|
+
`Carthagent Cloud account ${account?.accountId || 'unknown'}`,
|
|
260
265
|
`Credit: ${formatNanoUsd(account?.balance?.availableNanoUsd)} available`,
|
|
261
|
-
`Plan: ${subscription.status && subscription.status !== 'none' ? `${planName} (${subscription.status})` : '
|
|
266
|
+
`Plan: ${subscription.status && subscription.status !== 'none' ? `${planName} (${subscription.status})` : 'No paid plan'}`,
|
|
267
|
+
...(account?.email ? [`Signed in as: ${account.email}`] : []),
|
|
262
268
|
];
|
|
263
269
|
const subscriptionEnd = formatEpochSeconds(subscription.current_period_end);
|
|
264
270
|
if (subscriptionEnd) lines.push(`Renews/ends: ${subscriptionEnd}`);
|
|
@@ -100,8 +100,9 @@ function errorText(error) {
|
|
|
100
100
|
function headlessFailureMessage(latest, headlessError) {
|
|
101
101
|
const raw = errorText(latest?.error ?? latest?.session?.error ?? lastAssistantError(latest?.messages) ?? headlessError);
|
|
102
102
|
const outcome = latest?.outcome ?? latest?.session?.outcome;
|
|
103
|
+
if (['elapsed-time', 'tool-calls', 'repair-rounds'].includes(latest?.budgetReason)) return `Session execution budget exhausted: ${latest.budgetReason}. Billing refresh does not reset execution limits. To authorize a fresh budget, resume this session with --reset-budget (for example: ctg --print --continue --reset-budget "Continue the task"), or run /delivery-budget-reset in the existing terminal session before continuing.`;
|
|
103
104
|
if (/ModelNotSelectedError|No model is available|no provider|provider.*not (connected|selected|configured)|not authenticated/i.test(raw ?? '')) {
|
|
104
|
-
return 'No AI provider connected. Run `ctg login` to connect one (Carthagent
|
|
105
|
+
return 'No AI provider connected. Run `ctg login` to connect one (Carthagent Cloud is recommended), or set an API key env var (e.g. ANTHROPIC_API_KEY), then retry. Run `ctg native --list-models` to see available models.';
|
|
105
106
|
}
|
|
106
107
|
if (raw) return `Native run failed: ${raw}`;
|
|
107
108
|
if (latest?.reason || latest?.budgetReason) return `Native run failed: ${latest.reason ?? latest.budgetReason}`;
|
|
@@ -311,10 +312,10 @@ function shipLoginGuidance(error) {
|
|
|
311
312
|
const message = String(error?.data?.message || error?.message || '');
|
|
312
313
|
const type = String(error?.type || error?.data?.type || '');
|
|
313
314
|
if (type === 'ship.unauthenticated' || type === 'ship.not-connected') {
|
|
314
|
-
return 'Carthagent
|
|
315
|
+
return 'Carthagent Cloud is not connected. Run `ctg login` and choose Carthagent Cloud, then retry.';
|
|
315
316
|
}
|
|
316
|
-
if (/active Carthagent Ship OAuth connection is required|not authenticated|unauthorized/i.test(message)) {
|
|
317
|
-
return 'Carthagent
|
|
317
|
+
if (/active Carthagent (?:Ship|Cloud) OAuth connection is required|not authenticated|unauthorized/i.test(message)) {
|
|
318
|
+
return 'Carthagent Cloud is not connected. Run `ctg login` and choose Carthagent Cloud, then retry.';
|
|
318
319
|
}
|
|
319
320
|
}
|
|
320
321
|
|
|
@@ -559,6 +560,9 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
559
560
|
options.session = await latestSessionForWorkspace(client, cwd);
|
|
560
561
|
if (!options.session) throw new Error('No native Carthagent session exists for this workspace to continue.');
|
|
561
562
|
}
|
|
563
|
+
if (options.resetBudget && !options.session && !options.continue && !options.resume) {
|
|
564
|
+
throw new Error('--reset-budget requires --session, --continue or --resume.');
|
|
565
|
+
}
|
|
562
566
|
eventController = new AbortController();
|
|
563
567
|
const nativeEvents = [];
|
|
564
568
|
let eventFailure;
|
|
@@ -600,6 +604,9 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
600
604
|
const definitions = native.rpcDefinitions ?? await nativeRpcDefinitions();
|
|
601
605
|
await client.rpc(definitions.SessionControls).framing({ sessionID, enabled: options.autoFraming }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
602
606
|
}
|
|
607
|
+
if (options.resetBudget && sessionID) {
|
|
608
|
+
await client.session.command({ sessionID, name: 'delivery-budget-reset', text: '' }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
609
|
+
}
|
|
603
610
|
if (framedPrompt) {
|
|
604
611
|
if (!sessionID) {
|
|
605
612
|
const created = await headlessApi.createSession(service, cwd, { body: {
|
|
@@ -644,6 +651,11 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
644
651
|
const definitions = native.rpcDefinitions ?? await nativeRpcDefinitions();
|
|
645
652
|
await client.rpc(definitions.SessionControls).framing({ sessionID: requestedSession, enabled: options.autoFraming }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
646
653
|
}
|
|
654
|
+
if (requestedSession && options.resetBudget && !signal.controller.signal.aborted) {
|
|
655
|
+
const resumed = await client.session.get({ sessionID: requestedSession }, { signal: signal.controller.signal });
|
|
656
|
+
assertSessionWorkspace(resumed, cwd);
|
|
657
|
+
await client.session.command({ sessionID: requestedSession, name: 'delivery-budget-reset', text: '' }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
658
|
+
}
|
|
647
659
|
try {
|
|
648
660
|
result = await headlessApi.runHeadless(service, cwd, framedPrompt || prompt || '', {
|
|
649
661
|
sessionID: options.session,
|
|
@@ -658,7 +670,16 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
658
670
|
headlessError = error;
|
|
659
671
|
result = { session: { id: options.session }, messages: [], outcome: signal.controller.signal.aborted ? 'interrupted' : 'failed', terminal: true, error: error.message };
|
|
660
672
|
}
|
|
661
|
-
|
|
673
|
+
let latest = headlessError ? result : await reconcileHeadlessResult(result, { client, signal: signal.controller.signal });
|
|
674
|
+
if (latest.outcome === 'interrupted' && !latest.error && !latest.session?.error && !lastAssistantError(latest.messages) && latest.session?.id && typeof client.rpc === 'function' && !signal.controller.signal.aborted) {
|
|
675
|
+
// An interrupt may carry no error in the host projection. Read the
|
|
676
|
+
// authoritative persistent budget without restarting the task.
|
|
677
|
+
try {
|
|
678
|
+
const definitions = native.rpcDefinitions ?? await nativeRpcDefinitions();
|
|
679
|
+
const budget = await client.rpc(definitions.Carthagent).budgetStatus({ sessionID: latest.session.id }, { location: { directory: cwd }, signal: AbortSignal.any([signal.controller.signal, AbortSignal.timeout(2000)]) });
|
|
680
|
+
if (budget?.stopped) latest = { ...latest, budgetReason: budget.stopped, error: latest.error ?? headlessFailureMessage({ budgetReason: budget.stopped }) };
|
|
681
|
+
} catch { /* Keep the original failure when budget status is unavailable. */ }
|
|
682
|
+
}
|
|
662
683
|
eventController.abort();
|
|
663
684
|
await eventReader;
|
|
664
685
|
if (eventFailure) throw new Error(`Native Carthagent event stream failed: ${eventFailure.message}`, { cause: eventFailure });
|
|
@@ -720,7 +741,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
720
741
|
if (!rows.length) {
|
|
721
742
|
io.error(term
|
|
722
743
|
? `No models match "${query.trim()}". Run \`ctg native --list-models\` to see every available model, or \`ctg login\` to connect a provider.`
|
|
723
|
-
: 'No AI provider connected, so no models are available. Run `ctg login` to connect one (Carthagent
|
|
744
|
+
: 'No AI provider connected, so no models are available. Run `ctg login` to connect one (Carthagent Cloud is recommended), or set an API key env var (e.g. ANTHROPIC_API_KEY), then run `ctg native --list-models` again.');
|
|
724
745
|
}
|
|
725
746
|
return rows.length;
|
|
726
747
|
}
|
|
@@ -741,6 +762,12 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
741
762
|
let integration;
|
|
742
763
|
let attempt;
|
|
743
764
|
let completed = false;
|
|
765
|
+
const connected = () => {
|
|
766
|
+
completed = true;
|
|
767
|
+
io.log(`Connected to ${integration.name}.`);
|
|
768
|
+
io.log('Run ctg in your project. Start small: “Find one failing test, explain the cause, and fix it with a regression test.”');
|
|
769
|
+
if (integration.id === 'experiential-labs') io.log('Use /account to check the connected account and available usage. Starter usage is granted only when eligible.');
|
|
770
|
+
};
|
|
744
771
|
try {
|
|
745
772
|
({ client } = await serviceClient());
|
|
746
773
|
location = { directory: cwd };
|
|
@@ -809,7 +836,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
809
836
|
}
|
|
810
837
|
if (method.type === 'external') {
|
|
811
838
|
await client.integration.connect.external({ integrationID: integration.id, methodID: method.id, answer, location }, { signal: controller.signal });
|
|
812
|
-
|
|
839
|
+
connected();
|
|
813
840
|
return 0;
|
|
814
841
|
}
|
|
815
842
|
if (method.type === 'key') {
|
|
@@ -817,8 +844,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
817
844
|
const key = secretField ? answer[secretField.key] : await questionMasked(stdin, stdout, `${integration.name} API key`, controller.signal);
|
|
818
845
|
if (typeof key !== 'string' || !key.trim()) throw new Error('An API key is required.');
|
|
819
846
|
await client.integration.connect.key({ integrationID: integration.id, key, answer, location }, { signal: controller.signal });
|
|
820
|
-
|
|
821
|
-
completed = true;
|
|
847
|
+
connected();
|
|
822
848
|
return 0;
|
|
823
849
|
}
|
|
824
850
|
if (method.type !== 'oauth') throw new Error(`Authentication method ${method.type} is not supported by native CLI login.`);
|
|
@@ -838,15 +864,14 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
838
864
|
code = await questionWithAbort(prompt, 'Paste the authorization code: ', controller.signal);
|
|
839
865
|
if (!code.trim()) throw new Error('Authorization code is required.');
|
|
840
866
|
await client.integration.oauth.complete({ integrationID: integration.id, attemptID: attempt.attemptID, code: code.trim(), location }, { signal: controller.signal });
|
|
841
|
-
|
|
842
|
-
completed = true;
|
|
867
|
+
connected();
|
|
843
868
|
return 0;
|
|
844
869
|
}
|
|
845
870
|
io.log('Waiting for authorization… (Ctrl+C to cancel)');
|
|
846
871
|
while (!controller.signal.aborted && Date.now() < attempt.time.expires) {
|
|
847
872
|
const response = await client.integration.oauth.status({ integrationID: integration.id, attemptID: attempt.attemptID, location }, { signal: controller.signal });
|
|
848
873
|
const status = response.data;
|
|
849
|
-
if (status.status === 'complete') {
|
|
874
|
+
if (status.status === 'complete') { connected(); return 0; }
|
|
850
875
|
if (status.status === 'failed') throw new Error(status.message || 'Authorization failed.');
|
|
851
876
|
if (status.status === 'expired') throw new Error('Authorization expired.');
|
|
852
877
|
await new Promise((resolve, reject) => {
|
|
@@ -929,7 +954,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
929
954
|
const auth = await shipCredential();
|
|
930
955
|
let result;
|
|
931
956
|
try { result = (await auth.rpc.shipAccount({}, { location: { directory: cwd } })).account; }
|
|
932
|
-
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent
|
|
957
|
+
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Cloud: ${error.message}`); }
|
|
933
958
|
const cloud = await cloudClient('');
|
|
934
959
|
if (args.includes('--json')) io.log(JSON.stringify(result));
|
|
935
960
|
else {
|
|
@@ -942,13 +967,26 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
942
967
|
return 0;
|
|
943
968
|
}
|
|
944
969
|
|
|
945
|
-
async function billing({ action }) {
|
|
970
|
+
async function billing({ action, plan }) {
|
|
971
|
+
if (action === 'refresh') {
|
|
972
|
+
const result = await account();
|
|
973
|
+
io.log('Return to your existing session. When usage is available, explicitly continue the task.');
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
946
976
|
const auth = await shipCredential();
|
|
947
977
|
let result;
|
|
948
|
-
try {
|
|
949
|
-
|
|
978
|
+
try {
|
|
979
|
+
if (action === 'checkout') {
|
|
980
|
+
const { quote } = await auth.rpc.shipBilling({ action: 'quote', ...(plan ? { plan } : {}) }, { location: { directory: cwd } });
|
|
981
|
+
io.log(quote.terms);
|
|
982
|
+
if (quote.changingPlan) io.log('Review the exact proration in your browser before confirming the change.');
|
|
983
|
+
}
|
|
984
|
+
result = await auth.rpc.shipBilling({ action, ...(plan ? { plan } : {}) }, { location: { directory: cwd } });
|
|
985
|
+
}
|
|
986
|
+
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Cloud: ${error.message}`); }
|
|
950
987
|
const opened = await openBrowserImpl(result.url).catch(() => false);
|
|
951
988
|
io.log(`${opened ? 'Opened' : 'Open'} ${result.url}`);
|
|
989
|
+
io.log('After payment, run ctg billing refresh. Return to your existing session and explicitly continue the task.');
|
|
952
990
|
return 0;
|
|
953
991
|
}
|
|
954
992
|
|
|
@@ -956,11 +994,11 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
956
994
|
const auth = await shipCredential();
|
|
957
995
|
let result;
|
|
958
996
|
try { result = await auth.rpc.shipLogout({ ...(sessionID ? { session: sessionID } : {}) }, { location: { directory: cwd } }); }
|
|
959
|
-
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent
|
|
997
|
+
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Cloud logout: ${error.message}`); }
|
|
960
998
|
if (!sessionID && result.credentialID) {
|
|
961
999
|
await auth.client.credential.remove({ credentialID: result.credentialID });
|
|
962
1000
|
}
|
|
963
|
-
io.log(sessionID ? `Revoked
|
|
1001
|
+
io.log(sessionID ? `Revoked Cloud session ${sessionID}.` : 'Revoked and removed the current Carthagent Cloud session.');
|
|
964
1002
|
return 0;
|
|
965
1003
|
}
|
|
966
1004
|
|
package/lib/opencode/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const BOOL_FLAGS = new Map([
|
|
|
26
26
|
['--protect-existing', ['protectExisting', true]], ['--jev', ['jev', true]], ['--no-jev', ['jev', false]],
|
|
27
27
|
['--print', ['print', true]], ['--demo', ['demo', true]], ['-c', ['continue', true]],
|
|
28
28
|
['--continue', ['continue', true]], ['-r', ['resume', true]], ['--resume', ['resume', true]],
|
|
29
|
+
['--reset-budget', ['resetBudget', true]],
|
|
29
30
|
]);
|
|
30
31
|
|
|
31
32
|
const aliases = new Map([
|
|
@@ -84,9 +85,9 @@ export function cliHelp(version = '') {
|
|
|
84
85
|
\x1b[32mhash\x1b[0m, \x1b[32mfingerprint\x1b[0m Calculate workspace SHA-256 fingerprint
|
|
85
86
|
\x1b[32mvalidate\x1b[0m <file.json> Validate acceptance contract
|
|
86
87
|
\x1b[32mlogin\x1b[0m, \x1b[32mauth\x1b[0m Connect an AI provider
|
|
87
|
-
\x1b[32maccount\x1b[0m Show
|
|
88
|
-
\x1b[32mbilling\x1b[0m [checkout|portal] Open
|
|
89
|
-
\x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named
|
|
88
|
+
\x1b[32maccount\x1b[0m Show Cloud credit, plan, grants and CLI sessions
|
|
89
|
+
\x1b[32mbilling\x1b[0m [checkout|portal|refresh] Open or refresh Cloud billing
|
|
90
|
+
\x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named Cloud session
|
|
90
91
|
\x1b[32marchive\x1b[0m [list|search|read|export] Read-only legacy session archive
|
|
91
92
|
\x1b[32mreview\x1b[0m Configure self-review or review a PR
|
|
92
93
|
\x1b[32mserve\x1b[0m, \x1b[32mweb\x1b[0m, \x1b[32mstart\x1b[0m Start the web dashboard
|
|
@@ -110,6 +111,8 @@ export function cliHelp(version = '') {
|
|
|
110
111
|
--max-tools <n> Session tool-call budget (0 disables)
|
|
111
112
|
--max-seconds <n> Session elapsed-time budget (0 disables)
|
|
112
113
|
--max-repairs <n> Automatic-repair budget (0 disables)
|
|
114
|
+
--reset-budget Explicitly reset a resumed session's execution budget
|
|
115
|
+
Budgets persist across continuation, including idle time.
|
|
113
116
|
--protect-existing Preserve files present at session start
|
|
114
117
|
--rewrite-cap <n> Whole-file writes per path (0 disables)
|
|
115
118
|
--tool-output-cap <n> Bound text output per tool result
|
|
@@ -189,15 +192,16 @@ export async function runOpenCodeCli({ argv = [], runtime, io = console, stdoutI
|
|
|
189
192
|
return fail(error.message, 1, io);
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
|
-
if (normalized === 'logout') return fail('Use `logout-cloud` to revoke a Carthagent
|
|
195
|
+
if (normalized === 'logout') return fail('Use `logout-cloud` to revoke a Carthagent Cloud session; provider credentials are managed by the native runtime.', 1, io);
|
|
193
196
|
if (normalized === 'account' || normalized === 'cloud') {
|
|
194
197
|
try { return Number((await ensureMethod(runtime, 'account')({ args: rest })) ?? 0); }
|
|
195
198
|
catch (error) { return fail(error.message, 1, io); }
|
|
196
199
|
}
|
|
197
200
|
if (normalized === 'billing') {
|
|
198
201
|
const action = rest[0] || 'checkout';
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
const plan = rest[1];
|
|
203
|
+
if (!['checkout', 'portal', 'refresh'].includes(action) || (plan && !['builder', 'pro', 'max'].includes(plan))) return fail('Usage: ctg billing [checkout [builder|pro|max]|portal|refresh]', 1, io);
|
|
204
|
+
try { return Number((await ensureMethod(runtime, 'billing')({ action, ...(plan ? { plan } : {}) })) ?? 0); }
|
|
201
205
|
catch (error) { return fail(error.message, 1, io); }
|
|
202
206
|
}
|
|
203
207
|
if (normalized === 'logout-cloud') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const NATIVE_LOGIN_PROVIDERS = Object.freeze([
|
|
2
|
-
Object.freeze({ id: 'experiential-labs', name: 'Carthagent
|
|
2
|
+
Object.freeze({ id: 'experiential-labs', name: 'Carthagent Cloud', aliases: ['cloud', 'ship', 'carthagent ship'], oauth: 'carthagent-device' }),
|
|
3
3
|
Object.freeze({ id: 'anthropic', name: 'Claude / Anthropic', aliases: ['claude', 'claudecode'] }),
|
|
4
4
|
Object.freeze({ id: 'xai', name: 'xAI / Grok', aliases: ['grok', 'xai-code'], oauth: 'device' }),
|
|
5
5
|
Object.freeze({ id: 'openai', name: 'Codex / OpenAI', aliases: ['codex', 'openai-codex'], oauth: 'chatgpt-token-sharing' }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"releaseVersion": "1.1.0-beta.
|
|
3
|
+
"releaseVersion": "1.1.0-beta.3",
|
|
4
4
|
"compatibilityVersion": "2.0.32",
|
|
5
5
|
"runtimeVersion": "0.0.0-ctg.2.0.32",
|
|
6
6
|
"sourceRevision": "843a6c514e606b436a7fe713422ace0a93ad3d61",
|
|
@@ -9,28 +9,28 @@
|
|
|
9
9
|
"assets": {
|
|
10
10
|
"linux-x64": {
|
|
11
11
|
"name": "opencode-linux-x64.tar.gz",
|
|
12
|
-
"archiveSha256": "
|
|
13
|
-
"executableSha256": "
|
|
12
|
+
"archiveSha256": "ab95bd99fc2bf52f867927a817dde1d3882122b80f85ef85cf416733afa62483",
|
|
13
|
+
"executableSha256": "f9ad13d8aad76bf1d16334712f0060b886757f5cdabddacce42f9805d4482c1c"
|
|
14
14
|
},
|
|
15
15
|
"linux-arm64": {
|
|
16
16
|
"name": "opencode-linux-arm64.tar.gz",
|
|
17
|
-
"archiveSha256": "
|
|
18
|
-
"executableSha256": "
|
|
17
|
+
"archiveSha256": "3869dd9398693afffe9f438192fbb12061b66a211993018379fb6870d2012bd2",
|
|
18
|
+
"executableSha256": "0c302d55bba608d83c42f54b769ec3994421d3dcb16ea5d2a9455dcd91f884a1"
|
|
19
19
|
},
|
|
20
20
|
"darwin-x64": {
|
|
21
21
|
"name": "opencode-darwin-x64.tar.gz",
|
|
22
|
-
"archiveSha256": "
|
|
23
|
-
"executableSha256": "
|
|
22
|
+
"archiveSha256": "b20111d8fd2368ac9bde7f90b12835a465b8c92da3db1f3a35671860a325eb27",
|
|
23
|
+
"executableSha256": "23f7a845e75592efa11dff407e147287584eae7f63fca1a9c4ccbe9558656bc3"
|
|
24
24
|
},
|
|
25
25
|
"darwin-arm64": {
|
|
26
26
|
"name": "opencode-darwin-arm64.tar.gz",
|
|
27
|
-
"archiveSha256": "
|
|
28
|
-
"executableSha256": "
|
|
27
|
+
"archiveSha256": "eb65c635c8cf19f0830b56b2d8087387148edaf44cf821c881b72388c9c185fd",
|
|
28
|
+
"executableSha256": "d10a7b549748a131c578ed8cd041aeb652f1ae9c79c5f3912a731f2a69218736"
|
|
29
29
|
},
|
|
30
30
|
"windows-x64": {
|
|
31
31
|
"name": "opencode-windows-x64.tar.gz",
|
|
32
|
-
"archiveSha256": "
|
|
33
|
-
"executableSha256": "
|
|
32
|
+
"archiveSha256": "a0309761781f57b5ea9f329d41f2e977391949addc9baaca6b85025780249d3c",
|
|
33
|
+
"executableSha256": "91eac290633313c7fce4a426ef265f193e995b285746df6a2a6010f4727fe813"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
package/lib/providers/names.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CARTHAGENT_SHIP_PROVIDER_ID = 'experiential-labs';
|
|
2
|
-
export const CARTHAGENT_SHIP_PROVIDER_NAME = 'Carthagent
|
|
2
|
+
export const CARTHAGENT_SHIP_PROVIDER_NAME = 'Carthagent Cloud';
|
|
3
3
|
|
|
4
4
|
export function providerDisplayName(providerId) {
|
|
5
5
|
return providerId === CARTHAGENT_SHIP_PROVIDER_ID ? CARTHAGENT_SHIP_PROVIDER_NAME : String(providerId || '');
|
package/lib/tui/models.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function providerForModel(model, list) {
|
|
|
30
30
|
export function modelMatchesQuery(model, query) {
|
|
31
31
|
const tokens = String(query || '').toLowerCase().match(/[\p{L}\p{N}]+/gu) || [];
|
|
32
32
|
if (!tokens.length) return true;
|
|
33
|
-
const searchable = [model?.provider, providerDisplayName(model?.provider), model?.id, model?.full, model?.name]
|
|
33
|
+
const searchable = [model?.provider, providerDisplayName(model?.provider), model?.provider === 'experiential-labs' ? 'Carthagent Ship' : '', model?.id, model?.full, model?.name]
|
|
34
34
|
.filter(Boolean)
|
|
35
35
|
.join(' ')
|
|
36
36
|
.toLowerCase();
|
package/package.json
CHANGED
|
@@ -95,8 +95,8 @@ export const Carthagent = Rpc.define({
|
|
|
95
95
|
output: { type: 'object', properties: { usage: { type: 'object', additionalProperties: true } }, required: ['usage'], additionalProperties: false },
|
|
96
96
|
},
|
|
97
97
|
shipBilling: {
|
|
98
|
-
input: { type: 'object', properties: { action: { type: 'string', enum: ['checkout', 'portal'] } }, required: ['action'], additionalProperties: false },
|
|
99
|
-
output: { type: 'object', properties: { url: { type: 'string' }
|
|
98
|
+
input: { type: 'object', properties: { action: { type: 'string', enum: ['checkout', 'portal', 'quote'] }, plan: { type: 'string', enum: ['builder', 'pro', 'max'] } }, required: ['action'], additionalProperties: false },
|
|
99
|
+
output: { type: 'object', properties: { url: { type: 'string' }, quote: { type: 'object', additionalProperties: true } }, additionalProperties: false },
|
|
100
100
|
errors: { unauthenticated: { type: 'object', properties: {}, additionalProperties: false } },
|
|
101
101
|
},
|
|
102
102
|
shipLogout: {
|
|
@@ -120,19 +120,18 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
120
120
|
return {
|
|
121
121
|
integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID),
|
|
122
122
|
method: {
|
|
123
|
-
id: OAUTH_METHOD, type: 'oauth' as const, label: '
|
|
124
|
-
form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy
|
|
125
|
-
description: 'Leave disabled for fresh browser approval and updated account permissions.', default: false }] satisfies Form.Fields,
|
|
123
|
+
id: OAUTH_METHOD, type: 'oauth' as const, label: 'Continue with Carthagent Cloud',
|
|
124
|
+
...(env.CARTHAGENT_ADVANCED_AUTH === '1' ? { form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy Cloud credential?', default: false }] satisfies Form.Fields } : {}),
|
|
126
125
|
},
|
|
127
126
|
authorize: async (answer: Form.Answer) => {
|
|
128
127
|
// The legacy file intentionally survives migration. Never let its presence
|
|
129
128
|
// short-circuit an explicit sign-in or recovery from revoked/old scopes.
|
|
130
|
-
const legacy = answer.importLegacy === true ? await legacyOAuthForOpenCode({ agentDir }) : undefined;
|
|
129
|
+
const legacy = env.CARTHAGENT_ADVANCED_AUTH === '1' && answer.importLegacy === true ? await legacyOAuthForOpenCode({ agentDir }) : undefined;
|
|
131
130
|
if (legacy) {
|
|
132
131
|
return {
|
|
133
132
|
mode: 'auto' as const,
|
|
134
133
|
url: 'https://api.carthagent.xyz',
|
|
135
|
-
instructions: 'A legacy Carthagent
|
|
134
|
+
instructions: 'A legacy Carthagent Cloud credential was found and will be migrated into native integration storage.',
|
|
136
135
|
callback: Promise.resolve({
|
|
137
136
|
type: 'oauth' as const,
|
|
138
137
|
methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
|
|
@@ -147,7 +146,7 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
147
146
|
Promise.resolve(openBrowserImpl(url)).catch(() => false);
|
|
148
147
|
return {
|
|
149
148
|
mode: 'auto' as const, url,
|
|
150
|
-
instructions: `
|
|
149
|
+
instructions: `Sign in and connect this terminal using code ${response.userCode}. Check the account shown in your browser, then return here. If the code expires, start login again.`,
|
|
151
150
|
expiresAt: now() + Number(response.expiresIn || 600) * 1000,
|
|
152
151
|
callback,
|
|
153
152
|
};
|
|
@@ -169,13 +168,15 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
169
168
|
});
|
|
170
169
|
},
|
|
171
170
|
),
|
|
172
|
-
label: () => 'Carthagent
|
|
171
|
+
label: () => 'Carthagent Cloud',
|
|
173
172
|
};
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
function integrationDefinitions(editor: Parameters<Context['integration']['transform']>[0] extends (editor: infer E) => void ? E : never, options: Parameters<typeof oauthMethod>[0]) {
|
|
177
176
|
editor.method.update(oauthMethod(options));
|
|
178
|
-
|
|
177
|
+
if (options.env.CARTHAGENT_ADVANCED_AUTH === '1') {
|
|
178
|
+
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Advanced: use an Experiential API key' } });
|
|
179
|
+
}
|
|
179
180
|
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'env', names: [EXPERIENTIAL_API_KEY_ENV] } });
|
|
180
181
|
editor.update(SHIP_INTEGRATION_ID, integration => { integration.name = EXPERIENTIAL_PROVIDER_NAME; });
|
|
181
182
|
}
|
|
@@ -197,7 +198,7 @@ async function loadShipModels({ ctx, fetchImpl, env, baseURL, signal }: {
|
|
|
197
198
|
const response = await fetchImpl(`${managed ? cloudGatewayUrl(env) : baseURL}/models`, {
|
|
198
199
|
headers: { authorization: `Bearer ${key}` }, signal: signalWithTimeout(signal, DISCOVERY_TIMEOUT),
|
|
199
200
|
});
|
|
200
|
-
if (!response.ok) throw new Error(`Carthagent
|
|
201
|
+
if (!response.ok) throw new Error(`Carthagent Cloud model discovery failed (${response.status}).`);
|
|
201
202
|
const models = parseExperientialModels(await response.json()).slice(0, PROFILE_CAP);
|
|
202
203
|
if (connection && managed) return { connection, models, managed };
|
|
203
204
|
return { connection, models, managed };
|
|
@@ -4,7 +4,7 @@ import type { ConnectionInfo } from '@opencode/client';
|
|
|
4
4
|
|
|
5
5
|
type ShipContext = Pick<Context, 'integration'>;
|
|
6
6
|
type ActiveCredential = Extract<ConnectionInfo, { type: 'credential' }>;
|
|
7
|
-
type CloudClientPort = Pick<CloudClient, 'account' | 'usage' | 'checkout' | 'portal' | 'revokeCurrent' | 'revokeSession'>;
|
|
7
|
+
type CloudClientPort = Pick<CloudClient, 'account' | 'usage' | 'quote' | 'checkout' | 'portal' | 'revokeCurrent' | 'revokeSession'>;
|
|
8
8
|
|
|
9
9
|
/** Create profile-scoped Ship RPC handlers; injection keeps cloud behavior testable offline. */
|
|
10
10
|
export function createShipRpcHandlers(ctx: ShipContext, options: {
|
|
@@ -15,10 +15,10 @@ export function createShipRpcHandlers(ctx: ShipContext, options: {
|
|
|
15
15
|
const cloud = options.cloudClient ?? new CloudClient({ baseUrl: options.baseUrl ?? cloudControlUrl() });
|
|
16
16
|
async function access() {
|
|
17
17
|
const active = await ctx.integration.connection.active('experiential-labs');
|
|
18
|
-
if (!active || active.type !== 'credential' || active.method !== 'oauth') throw new Error('An active Carthagent
|
|
18
|
+
if (!active || active.type !== 'credential' || active.method !== 'oauth') throw new Error('An active Carthagent Cloud OAuth connection is required.');
|
|
19
19
|
const credential = await ctx.integration.connection.resolve(active);
|
|
20
20
|
if (credential?.type !== 'oauth' || credential.methodID !== 'carthagent-device' || !credential.access) {
|
|
21
|
-
throw new Error('An active Carthagent
|
|
21
|
+
throw new Error('An active Carthagent Cloud OAuth connection is required.');
|
|
22
22
|
}
|
|
23
23
|
return { active: active as ActiveCredential, token: credential.access };
|
|
24
24
|
}
|
|
@@ -27,27 +27,29 @@ export function createShipRpcHandlers(ctx: ShipContext, options: {
|
|
|
27
27
|
call.signal.throwIfAborted();
|
|
28
28
|
let token: string;
|
|
29
29
|
try { ({ token } = await access()); }
|
|
30
|
-
catch { return call.error('unauthenticated', 'An active Carthagent
|
|
30
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
|
|
31
31
|
return { account: await cloud.account({ accessToken: token, signal: call.signal }) };
|
|
32
32
|
},
|
|
33
33
|
shipUsage: async (input: unknown, call: { signal: AbortSignal }) => {
|
|
34
34
|
call.signal.throwIfAborted();
|
|
35
35
|
const { token } = await access();
|
|
36
36
|
const requested = (input as { limit?: number }).limit ?? 50;
|
|
37
|
-
|
|
37
|
+
if (!Number.isSafeInteger(requested) || requested < 1 || requested > 200) throw new Error('Cloud usage limit must be between 1 and 200.');
|
|
38
38
|
return { usage: await cloud.usage({ accessToken: token, limit: requested, signal: call.signal } as Parameters<CloudClient['usage']>[0]) };
|
|
39
39
|
},
|
|
40
40
|
shipBilling: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipBilling>) => {
|
|
41
41
|
call.signal.throwIfAborted();
|
|
42
42
|
let token: string;
|
|
43
43
|
try { ({ token } = await access()); }
|
|
44
|
-
catch { return call.error('unauthenticated', 'An active Carthagent
|
|
45
|
-
const action =
|
|
46
|
-
if (
|
|
44
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
|
|
45
|
+
const { action, plan = 'builder' } = input as { action: 'checkout' | 'portal' | 'quote'; plan?: string };
|
|
46
|
+
if (!['checkout', 'portal', 'quote'].includes(action)) throw new Error('Unsupported Cloud billing action.');
|
|
47
|
+
if (!['builder', 'pro', 'max'].includes(plan)) throw new Error('Choose Builder, Pro, or Max.');
|
|
48
|
+
if (action === 'quote') return { quote: await cloud.quote({ accessToken: token, plan, signal: call.signal }) };
|
|
47
49
|
const returnUrl = `${options.cloudUrl ?? cloudControlUrl()}/account`;
|
|
48
50
|
const result = action === 'portal'
|
|
49
51
|
? await cloud.portal({ accessToken: token, returnUrl, signal: call.signal })
|
|
50
|
-
: await cloud.checkout({ accessToken: token,
|
|
52
|
+
: await cloud.checkout({ accessToken: token, priceId: plan, successUrl: `${options.cloudUrl ?? cloudControlUrl()}/v1/browser/billing/complete`, cancelUrl: `${options.cloudUrl ?? cloudControlUrl()}/v1/browser/billing/cancelled?plan=${plan}`, signal: call.signal });
|
|
51
53
|
return { url: result.url };
|
|
52
54
|
},
|
|
53
55
|
shipLogout: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipLogout>) => {
|
|
@@ -55,7 +57,7 @@ export function createShipRpcHandlers(ctx: ShipContext, options: {
|
|
|
55
57
|
let active: ActiveCredential;
|
|
56
58
|
let token: string;
|
|
57
59
|
try { ({ active, token } = await access()); }
|
|
58
|
-
catch { return call.error('unauthenticated', 'An active Carthagent
|
|
60
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
|
|
59
61
|
const session = (input as { session?: string }).session;
|
|
60
62
|
if (session) {
|
|
61
63
|
await cloud.revokeSession({ accessToken: token, sessionId: session, signal: call.signal });
|
|
@@ -308,7 +308,7 @@ export default Plugin.define({
|
|
|
308
308
|
}
|
|
309
309
|
case "account": {
|
|
310
310
|
const result = await shipClient(context, location).account() as { account: Record<string, unknown> };
|
|
311
|
-
await context.ui.dialog.alert({ title: "Carthagent
|
|
311
|
+
await context.ui.dialog.alert({ title: "Carthagent Cloud account", message: cloudAccountText(result.account) });
|
|
312
312
|
return;
|
|
313
313
|
}
|
|
314
314
|
case "usage": {
|
|
@@ -316,24 +316,34 @@ export default Plugin.define({
|
|
|
316
316
|
shipClient(context, location).account() as Promise<{ account: Record<string, unknown> }>,
|
|
317
317
|
shipClient(context, location).usage({ limit: 50 }) as Promise<{ usage: Record<string, unknown> }>,
|
|
318
318
|
]);
|
|
319
|
-
await context.ui.dialog.alert({ title: "Carthagent
|
|
319
|
+
await context.ui.dialog.alert({ title: "Carthagent Cloud usage", message: cloudUsageText(usageResult.usage, accountResult.account) });
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
322
|
case "billing": {
|
|
323
|
-
const action = arg.toLowerCase()
|
|
324
|
-
if (
|
|
325
|
-
|
|
323
|
+
const [action = "checkout", plan = "builder"] = arg.toLowerCase().split(/\s+/).filter(Boolean);
|
|
324
|
+
if (action === "refresh") {
|
|
325
|
+
const result = await shipClient(context, location).account() as { account: Record<string, unknown> };
|
|
326
|
+
await context.ui.dialog.alert({ title: "Cloud access refreshed", message: `${cloudAccountText(result.account)}\n\nYour task stays in this session. When usage is available, send “Continue the task” to resume.` });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!["checkout", "portal"].includes(action) || !["builder", "pro", "max"].includes(plan)) { toast("Usage: /billing [checkout [builder|pro|max]|portal|refresh].", "warning"); return; }
|
|
330
|
+
if (action === "checkout") {
|
|
331
|
+
const { quote } = await shipClient(context, location).billing({ action: "quote", plan: plan as "builder" | "pro" | "max" }) as { quote: { terms: string; changingPlan: boolean } };
|
|
332
|
+
const proceed = await context.ui.dialog.confirm({ title: "Review Cloud plan", message: `${quote.terms}\n\n${quote.changingPlan ? "Review the exact proration in your browser before confirming the change." : "Continue to Stripe to review and pay."}\nYour task stays in this session. You can also connect your own provider with /login.` });
|
|
333
|
+
if (proceed !== true) return;
|
|
334
|
+
}
|
|
335
|
+
const { url } = await shipClient(context, location).billing({ action: action as "checkout" | "portal", plan: plan as "builder" | "pro" | "max" }) as { url: string };
|
|
326
336
|
const { openBrowser } = await import("../../../lib/cloud/client.mjs");
|
|
327
337
|
const opened = await openBrowser(url).catch(() => false);
|
|
328
|
-
await context.ui.dialog.alert({ title: action === "portal" ? "
|
|
338
|
+
await context.ui.dialog.alert({ title: action === "portal" ? "Cloud billing portal" : "Cloud checkout", message: `${opened ? "Opened in your browser." : "Open this URL in your browser:"}\n${url}\n\nAfter payment, use /billing refresh. Your task stays here; send “Continue the task” when ready.` });
|
|
329
339
|
return;
|
|
330
340
|
}
|
|
331
341
|
case "logout-cloud": {
|
|
332
342
|
const session = arg || undefined;
|
|
333
|
-
const confirmed = await context.ui.dialog.confirm({ title: session ? "Revoke
|
|
343
|
+
const confirmed = await context.ui.dialog.confirm({ title: session ? "Revoke Cloud session" : "Sign out of Carthagent Cloud", message: session ? `Revoke Cloud session ${session}?` : "Revoke the active Cloud session and remove its Carthagent connection?" });
|
|
334
344
|
if (confirmed !== true) return;
|
|
335
345
|
await shipClient(context, location).logout(session ? { session } : {});
|
|
336
|
-
toast(session ? `Revoked
|
|
346
|
+
toast(session ? `Revoked Cloud session ${session}.` : "Revoked and removed the active Carthagent Cloud connection.", "success");
|
|
337
347
|
return;
|
|
338
348
|
}
|
|
339
349
|
case "archive": {
|
|
@@ -95,7 +95,7 @@ export function shipClient(context: RpcContext, selectedLocation?: SelectedLocat
|
|
|
95
95
|
return {
|
|
96
96
|
account: (signal?: AbortSignal) => rpc.shipAccount({}, requestOptions(context, selectedLocation, signal)),
|
|
97
97
|
usage: (input: { limit?: number } = {}, signal?: AbortSignal) => rpc.shipUsage(input, requestOptions(context, selectedLocation, signal)),
|
|
98
|
-
billing: (input: { action: "checkout" | "portal" }, signal?: AbortSignal) => rpc.shipBilling(input, requestOptions(context, selectedLocation, signal)),
|
|
98
|
+
billing: (input: { action: "checkout" | "portal" | "quote"; plan?: "builder" | "pro" | "max" }, signal?: AbortSignal) => rpc.shipBilling(input, requestOptions(context, selectedLocation, signal)),
|
|
99
99
|
logout: (input: { session?: string } = {}, signal?: AbortSignal) => rpc.shipLogout(input, requestOptions(context, selectedLocation, signal)),
|
|
100
100
|
};
|
|
101
101
|
}
|
|
@@ -38,12 +38,12 @@ export const COMMANDS: readonly CarthagentCommand[] = [
|
|
|
38
38
|
{ id: "architecture", name: "architecture", description: "Open the delivery workspace architecture view" },
|
|
39
39
|
{ id: "evidence", name: "evidence", description: "Open observed delivery evidence" },
|
|
40
40
|
{ id: "login", name: "login", aliases: ["auth"], description: "Connect Ship, Claude, xAI, Codex/OpenAI, or Gemini" },
|
|
41
|
-
{ id: "account", name: "account", aliases: ["cloud"], description: "Show
|
|
41
|
+
{ id: "account", name: "account", aliases: ["cloud"], description: "Show Cloud account, credits, plan, and sessions" },
|
|
42
42
|
{ id: "voyages", name: "voyages", description: "Open Voyages profile, wallet, equipment, shop, and daily quests" },
|
|
43
43
|
{ id: "quests", name: "quests", description: "Open daily coding quests; evidence requires separate explicit consent" },
|
|
44
44
|
{ id: "usage", name: "usage", description: "Show recent Ship usage from the control plane" },
|
|
45
45
|
{ id: "billing", name: "billing", args: "[checkout|portal]", description: "Open Ship checkout or billing portal" },
|
|
46
|
-
{ id: "logout-cloud", name: "logout-cloud", args: "[session-id]", description: "Revoke a
|
|
46
|
+
{ id: "logout-cloud", name: "logout-cloud", args: "[session-id]", description: "Revoke a Cloud session or sign out this profile" },
|
|
47
47
|
{ id: "archive", name: "archive", args: "[search <query>|read <id>|export <id>]", description: "Browse bounded read-only archived sessions" },
|
|
48
48
|
{ id: "demo", name: "demo", description: "Run a scripted, provider-free delivery walkthrough" },
|
|
49
49
|
{ id: "ctg-workspace", name: "ctg-workspace", args: "[refresh]", description: "Browse and open local CTG workspace projects, tasks, and sessions" },
|
|
@@ -132,7 +132,7 @@ export function DailyQuestsPage(props: { sessionID?: string; questID?: string })
|
|
|
132
132
|
const state = view(), detail = state.detail;
|
|
133
133
|
const basic = [
|
|
134
134
|
{ name: "Refresh daily assignments and recovery journal", description: "Read-only; never uploads or replays evidence", value: "refresh" },
|
|
135
|
-
{ name: "Sign in / reconnect", description: "Native Carthagent
|
|
135
|
+
{ name: "Sign in / reconnect", description: "Native Carthagent Cloud device sign-in; not grading consent", value: "signin" },
|
|
136
136
|
{ name: "Back to Voyages", description: "Discard unsent editor contents", value: "back" },
|
|
137
137
|
];
|
|
138
138
|
if (!state.snapshot) return basic;
|
|
@@ -60,7 +60,7 @@ export function VoyagesPage(props: { sessionID?: string; section?: "profile" | "
|
|
|
60
60
|
const basic = [
|
|
61
61
|
{ name: "Refresh authoritative state", description: "Reload live profile and catalog", value: "refresh" },
|
|
62
62
|
{ name: "Daily coding quests", description: "Server assignments · selected evidence · explicit external JEV consent", value: "daily" },
|
|
63
|
-
{ name: "Sign in / reconnect", description: "Native provider connection · choose Carthagent
|
|
63
|
+
{ name: "Sign in / reconnect", description: "Native provider connection · choose Carthagent Cloud device sign-in", value: "signin" },
|
|
64
64
|
{ name: "Back", description: props.sessionID ? "Return to selected session" : "Return home", value: "back" },
|
|
65
65
|
...(props.section ? [{ name: props.section === "profile" ? "Equipment & shop" : "Profile & wallet", description: "Open the other Voyages detail view", value: "section" }] : []),
|
|
66
66
|
];
|
package/tests/cli.test.mjs
CHANGED
|
@@ -102,11 +102,11 @@ test('check fails clearly when no delivery report exists', t => {
|
|
|
102
102
|
assert.match(result.stderr, /No delivery report/);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test('guidance routing is not a user-facing command and
|
|
105
|
+
test('guidance routing is not a user-facing command and Cloud account commands are documented', t => {
|
|
106
106
|
const result = run(fixture(t), '--help');
|
|
107
107
|
assert.equal(result.status, 0, result.stderr);
|
|
108
108
|
assert.doesNotMatch(result.stdout, /\broute\b|\bprofiles\b/);
|
|
109
|
-
assert.match(result.stdout, /account.*
|
|
109
|
+
assert.match(result.stdout, /account.*Cloud credit/);
|
|
110
110
|
assert.match(result.stdout, /billing.*checkout\|portal/);
|
|
111
111
|
assert.match(result.stdout, /logout-cloud/);
|
|
112
112
|
});
|
|
@@ -90,7 +90,7 @@ test('Cloud client sends account, billing, and session requests with bearer auth
|
|
|
90
90
|
|
|
91
91
|
test('OAuth and account formatters keep token expiration and account UX deterministic', () => {
|
|
92
92
|
assert.deepEqual(oauthCredentials({ accessToken: 'a', refreshToken: 'r', expiresIn: 900 }, 1_000), { access: 'a', refresh: 'r', expires: 901_000 });
|
|
93
|
-
assert.match(formatCloudAccount({ accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [{}, { revoked_at: 'x' }] }), /Credit: \$1\.00 available[\s\S]*Plan:
|
|
93
|
+
assert.match(formatCloudAccount({ accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [{}, { revoked_at: 'x' }] }), /Credit: \$1\.00 available[\s\S]*Plan: No paid plan[\s\S]*1 active, 1 revoked/);
|
|
94
94
|
const managed = formatCloudAccount({
|
|
95
95
|
accountId: 'paid', balance: { availableNanoUsd: 8_000_000_000 },
|
|
96
96
|
subscription: { status: 'active', current_period_end: 1_800_000_000 },
|
|
@@ -123,7 +123,7 @@ test('OAuth and account formatters keep token expiration and account UX determin
|
|
|
123
123
|
test('managed usage recovery distinguishes daily, monthly, subscription, and credit paths', () => {
|
|
124
124
|
assert.match(cloudManagedUsageRecovery('daily_limit_reached'), /00:00 UTC/);
|
|
125
125
|
assert.match(cloudManagedUsageRecovery('monthly_allowance_exhausted'), /billing portal/);
|
|
126
|
-
assert.match(cloudManagedUsageRecovery({ code: 'subscription_inactive' }), /
|
|
126
|
+
assert.match(cloudManagedUsageRecovery({ code: 'subscription_inactive' }), /awaiting payment or activation/);
|
|
127
127
|
assert.match(cloudManagedUsageRecovery('insufficient_quota'), /billing checkout/);
|
|
128
128
|
assert.equal(cloudManagedUsageRecovery('upstream timeout'), null);
|
|
129
129
|
});
|
|
@@ -31,7 +31,7 @@ test('Cloud extension gives limit-specific recovery without hiding BYOK', () =>
|
|
|
31
31
|
assert.equal(notices.length, 3);
|
|
32
32
|
assert.match(notices[0][0], /00:00 UTC/);
|
|
33
33
|
assert.match(notices[1][0], /billing portal/);
|
|
34
|
-
assert.match(notices[2][0], /
|
|
34
|
+
assert.match(notices[2][0], /awaiting payment or activation/);
|
|
35
35
|
assert.ok(notices.every(([, tone]) => tone === 'warning'));
|
|
36
|
-
assert.ok(notices.every(([message]) => /\/login
|
|
36
|
+
assert.ok(notices.every(([message]) => /\/login/.test(message) && /BYOK|your own provider/.test(message)));
|
|
37
37
|
});
|
|
@@ -272,7 +272,7 @@ test('registerExperientialProvider registers one shared provider definition', ()
|
|
|
272
272
|
assert.equal(registerExperientialProvider(runtime), runtime);
|
|
273
273
|
assert.equal(calls.length, 1);
|
|
274
274
|
assert.equal(calls[0].id, 'experiential-labs');
|
|
275
|
-
assert.equal(calls[0].config.name, 'Carthagent
|
|
275
|
+
assert.equal(calls[0].config.name, 'Carthagent Cloud');
|
|
276
276
|
});
|
|
277
277
|
|
|
278
278
|
test('ModelRuntime resolves EXPLABS_API_KEY without copying it into provider config', async () => {
|
|
@@ -8,7 +8,9 @@ test('native login policy stays small, named, ordered and excludes upstream prov
|
|
|
8
8
|
assert.equal(nativeLoginProvider('opencode'), undefined);
|
|
9
9
|
assert.equal(nativeLoginProvider('opencode-go'), undefined);
|
|
10
10
|
assert.equal(nativeLoginProvider('openrouter'), undefined);
|
|
11
|
-
assert.equal(nativeProviderName('ship'), 'Carthagent
|
|
11
|
+
assert.equal(nativeProviderName('ship'), 'Carthagent Cloud');
|
|
12
|
+
assert.equal(nativeProviderName('Carthagent Ship'), 'Carthagent Cloud');
|
|
13
|
+
assert.equal(nativeProviderName('cloud'), 'Carthagent Cloud');
|
|
12
14
|
assert.equal(nativeLoginProvider('claudecode').id, 'anthropic');
|
|
13
15
|
assert.equal(nativeLoginProvider('codex').id, 'openai');
|
|
14
16
|
assert.equal(nativeLoginProvider('gemini').id, 'google');
|
|
@@ -20,7 +22,7 @@ test('curated login rows preserve actual methods and do not invent subscription
|
|
|
20
22
|
const ship = { id: 'experiential-labs', name: 'internal', methods: [{ type: 'oauth', id: 'carthagent-device' }] };
|
|
21
23
|
const rows = nativeLoginProviders([{ id: 'opencode', methods: [] }, anthropic, ship]);
|
|
22
24
|
assert.deepEqual(rows.map(row => row.id), ['experiential-labs', 'anthropic']);
|
|
23
|
-
assert.equal(rows[0].name, 'Carthagent
|
|
25
|
+
assert.equal(rows[0].name, 'Carthagent Cloud');
|
|
24
26
|
assert.deepEqual(rows[1].methods, [{ type: 'key' }]);
|
|
25
27
|
assert.equal(anthropic.name, 'Anthropic');
|
|
26
28
|
assert.equal(ship.name, 'internal');
|
|
@@ -20,6 +20,7 @@ function setup() {
|
|
|
20
20
|
const cloud = {
|
|
21
21
|
account: async (input) => { calls.push(['account', input]); return { email: 'fixture@example.test', sessions: [] }; },
|
|
22
22
|
usage: async (input) => { calls.push(['usage', input]); return { data: [{ alias: 'fixture-model', inputTokens: 12, outputTokens: 8 }] }; },
|
|
23
|
+
quote: async (input) => { calls.push(['quote', input]); return { introEligible: false, terms: 'Builder is $10/month until cancelled.' }; },
|
|
23
24
|
checkout: async (input) => { calls.push(['checkout', input]); return { url: 'https://billing.example.test/checkout' }; },
|
|
24
25
|
portal: async (input) => { calls.push(['portal', input]); return { url: 'https://billing.example.test/portal' }; },
|
|
25
26
|
revokeCurrent: async (input) => calls.push(['revokeCurrent', input]),
|
|
@@ -47,7 +48,18 @@ test('Ship account and billing RPCs use the active profile credential and inject
|
|
|
47
48
|
['portal', 'fixture-token', true],
|
|
48
49
|
]);
|
|
49
50
|
assert.ok(calls.some(([name, input]) => name === 'usage' && input.limit === 10));
|
|
50
|
-
assert.ok(calls.some(([name, input]) => name === 'checkout' && input.successUrl === 'https://ship.example.test/
|
|
51
|
+
assert.ok(calls.some(([name, input]) => name === 'checkout' && input.successUrl === 'https://ship.example.test/v1/browser/billing/complete' && input.priceId === 'builder'));
|
|
52
|
+
await handlers.shipBilling({ action: 'checkout', plan: 'max' }, call);
|
|
53
|
+
assert.equal(calls.at(-1)[1].priceId, 'max');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('native plan review uses authenticated server eligibility without creating checkout', async () => {
|
|
57
|
+
const { calls, handlers } = setup();
|
|
58
|
+
const result = await handlers.shipBilling({ action: 'quote', plan: 'builder' }, call);
|
|
59
|
+
assert.equal(result.quote.introEligible, false);
|
|
60
|
+
assert.equal(result.quote.terms, 'Builder is $10/month until cancelled.');
|
|
61
|
+
assert.ok(calls.some(([name, input]) => name === 'quote' && input.accessToken === 'fixture-token' && input.plan === 'builder' && input.signal === call.signal));
|
|
62
|
+
assert.equal(calls.some(([name]) => name === 'checkout'), false);
|
|
51
63
|
});
|
|
52
64
|
|
|
53
65
|
test('Ship logout revokes either one session or current token and removes current connection', async () => {
|
|
@@ -62,7 +74,7 @@ test('Ship logout revokes either one session or current token and removes curren
|
|
|
62
74
|
test('Ship RPC requires its managed OAuth credential and honors cancellation before cloud traffic', async () => {
|
|
63
75
|
const { ctx, calls, handlers } = setup();
|
|
64
76
|
ctx.integration.connection.resolve = async () => ({ type: 'oauth', methodID: 'other', access: 'wrong' });
|
|
65
|
-
await assert.rejects(handlers.shipAccount({}, call), { code: 'unauthenticated', message: 'An active Carthagent
|
|
77
|
+
await assert.rejects(handlers.shipAccount({}, call), { code: 'unauthenticated', message: 'An active Carthagent Cloud OAuth connection is required.' });
|
|
66
78
|
assert.equal(calls.some(([name]) => name === 'account'), false);
|
|
67
79
|
const controller = new AbortController();
|
|
68
80
|
controller.abort();
|
|
@@ -42,6 +42,40 @@ test('native CLI selects headless execution and preserves output-mode options',
|
|
|
42
42
|
assert.equal(invocation.prompt, 'Do work');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test('headless continuation explains persistent budget exhaustion and resets only on explicit request', async t => {
|
|
46
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
47
|
+
const { tmpdir } = await import('node:os');
|
|
48
|
+
const cwd = mkdtempSync(join(tmpdir(), 'ctg-budget-continuation-'));
|
|
49
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
50
|
+
const { io, output } = ioCapture();
|
|
51
|
+
const calls = [];
|
|
52
|
+
let stopped = 'elapsed-time';
|
|
53
|
+
const runtime = await createOpenCodeCliRuntime({ cwd, io, stdout: { write() {} }, native: {
|
|
54
|
+
runtime: {}, service: { ensureService: async () => ({}) }, rpcDefinitions: { Carthagent: {} },
|
|
55
|
+
clientFactory: async () => ({
|
|
56
|
+
rpc: () => ({ budgetStatus: async () => ({ stopped }) }),
|
|
57
|
+
session: {
|
|
58
|
+
list: async () => ({ data: [{ id: 'same-session', location: { directory: cwd } }] }),
|
|
59
|
+
get: async () => ({ id: 'same-session', location: { directory: cwd } }),
|
|
60
|
+
command: async input => { calls.push(input); stopped = null; },
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
headless: { runHeadless: async (_service, _cwd, _prompt, options) => {
|
|
64
|
+
assert.equal(options.sessionID, 'same-session');
|
|
65
|
+
calls.push('prompt');
|
|
66
|
+
return { session: { id: 'same-session' }, outcome: stopped ? 'interrupted' : 'succeeded', terminal: true,
|
|
67
|
+
messages: stopped ? [] : [{ type: 'assistant', finish: 'stop', content: [{ type: 'text', text: 'continued' }] }] };
|
|
68
|
+
} },
|
|
69
|
+
} });
|
|
70
|
+
assert.equal(parseOpenCodeArgs(['--continue', '--reset-budget', 'Continue']).resetBudget, true);
|
|
71
|
+
assert.equal(await runtime.run({ mode: 'headless', options: { continue: true }, prompt: 'Continue' }), 1);
|
|
72
|
+
assert.match(output.stderr.at(-1), /budget exhausted: elapsed-time.*--reset-budget/);
|
|
73
|
+
assert.deepEqual(calls, ['prompt'], 'no implicit budget reset or prompt retry');
|
|
74
|
+
assert.equal(await runtime.run({ mode: 'headless', options: { continue: true, resetBudget: true }, prompt: 'Continue' }), 0);
|
|
75
|
+
assert.deepEqual(calls.slice(1), [{ sessionID: 'same-session', name: 'delivery-budget-reset', text: '' }, 'prompt']);
|
|
76
|
+
await assert.rejects(runtime.run({ mode: 'headless', options: { resetBudget: true }, prompt: 'New task' }), /requires --session/);
|
|
77
|
+
});
|
|
78
|
+
|
|
45
79
|
test('headless CLI validates prompts and maps cancellation to conventional exit code', async () => {
|
|
46
80
|
const { io, output } = ioCapture();
|
|
47
81
|
assert.equal(await runOpenCodeCli({ argv: [], stdoutIsTTY: false, io, runtime: {} }), 1);
|
|
@@ -77,10 +111,10 @@ test('model, auth, cloud and domain entrypoints dispatch to injected native capa
|
|
|
77
111
|
]);
|
|
78
112
|
});
|
|
79
113
|
|
|
80
|
-
test('help preserves command aliases,
|
|
114
|
+
test('help preserves command aliases, Cloud operations and isolation semantics', async () => {
|
|
81
115
|
const { output, io } = ioCapture();
|
|
82
116
|
assert.equal(await runOpenCodeCli({ argv: ['--help'], io, version: '1.0.3' }), 0);
|
|
83
|
-
assert.match(output.stdout[0], /account.*
|
|
117
|
+
assert.match(output.stdout[0], /account.*Cloud credit/);
|
|
84
118
|
assert.match(output.stdout[0], /billing.*checkout\|portal/);
|
|
85
119
|
assert.match(output.stdout[0], /--isolate\s+Isolate extension\/skill discovery \(not a worktree\)/);
|
|
86
120
|
assert.equal(cliHelp('x').includes('--engine-cli, --agent-cli'), true);
|
|
@@ -1011,8 +1045,8 @@ test('native Ship billing and logout explain how to connect when the typed auth
|
|
|
1011
1045
|
shipLogout: async () => { throw Object.assign(new Error('Not authenticated'), { type: 'ship.unauthenticated' }); },
|
|
1012
1046
|
}) }),
|
|
1013
1047
|
} });
|
|
1014
|
-
await assert.rejects(runtime.billing({ action: 'checkout' }), /not connected.*ctg login.*Carthagent
|
|
1015
|
-
await assert.rejects(runtime.logout({}), /not connected.*ctg login.*Carthagent
|
|
1048
|
+
await assert.rejects(runtime.billing({ action: 'checkout' }), /not connected.*ctg login.*Carthagent Cloud/i);
|
|
1049
|
+
await assert.rejects(runtime.logout({}), /not connected.*ctg login.*Carthagent Cloud/i);
|
|
1016
1050
|
});
|
|
1017
1051
|
|
|
1018
1052
|
test('OAuth forms honor conditional fields, defaults, validation, and typed selections', async () => {
|
|
@@ -535,7 +535,7 @@ test('native sign-in starts fresh device authorization after opt-in legacy migra
|
|
|
535
535
|
const browserUrls = [];
|
|
536
536
|
let generation = 0;
|
|
537
537
|
const cleanup = await registerProviders(fake.ctx, {
|
|
538
|
-
env: { CARTHAGENT_CLOUD_URL: 'https://cloud.test' }, agentDir: directory, now: () => 1000,
|
|
538
|
+
env: { CARTHAGENT_CLOUD_URL: 'https://cloud.test', CARTHAGENT_ADVANCED_AUTH: '1' }, agentDir: directory, now: () => 1000,
|
|
539
539
|
openBrowser: url => { browserUrls.push(url); return false; },
|
|
540
540
|
fetchImpl: async (url, init) => {
|
|
541
541
|
requests.push({ url: String(url), body: JSON.parse(init.body) });
|
|
@@ -601,9 +601,10 @@ test('provider registration uses native provider transforms and cleanup; refresh
|
|
|
601
601
|
openBrowser: () => false, requestSecret: 'test-secret',
|
|
602
602
|
});
|
|
603
603
|
assert.equal(typeof cleanup, 'function');
|
|
604
|
-
assert.deepEqual(fake.integrationTransforms[0].map(item => item.integrationID), ['experiential-labs', 'experiential-labs'
|
|
604
|
+
assert.deepEqual(fake.integrationTransforms[0].map(item => item.integrationID), ['experiential-labs', 'experiential-labs']);
|
|
605
605
|
assert.equal(fake.integrationTransforms[0][0].method.id, 'carthagent-device');
|
|
606
|
-
assert.equal(fake.integrationTransforms[0][
|
|
606
|
+
assert.equal(fake.integrationTransforms[0][0].method.form, undefined, 'fresh sign-in has no migration question');
|
|
607
|
+
assert.equal(fake.integrationTransforms[0][1].method.type, 'env');
|
|
607
608
|
assert.equal(fake.providerTransforms.find(item => item.info?.id === providerConstants.SHIP_PROVIDER_ID).info.package,
|
|
608
609
|
'@opencode/ai/providers/openai-compatible');
|
|
609
610
|
assert.ok(fake.hooks.some(item => item.name === 'http.request' && item.options.providerID === 'experiential-labs'));
|
|
@@ -40,7 +40,7 @@ test('profile Ship RPC uses resolved native OAuth and loopback account, billing,
|
|
|
40
40
|
['DELETE', '/v1/account/sessions/session-target'], ['POST', '/v1/token/revoke'],
|
|
41
41
|
]);
|
|
42
42
|
assert.ok(requests.every(item => item.authorization === 'Bearer loopback-token'));
|
|
43
|
-
assert.deepEqual(requests[2].body, { successUrl: `${baseUrl}/
|
|
43
|
+
assert.deepEqual(requests[2].body, { successUrl: `${baseUrl}/v1/browser/billing/complete`, cancelUrl: `${baseUrl}/v1/browser/billing/cancelled?plan=builder`, priceId: 'builder' });
|
|
44
44
|
assert.deepEqual(requests[3].body, { returnUrl: `${baseUrl}/account` });
|
|
45
45
|
assert.deepEqual(removed, ['cred-profile']);
|
|
46
46
|
} finally {
|
|
@@ -61,7 +61,7 @@ test('profile Ship RPC rejects BYOK, absent auth, and pre-aborted operations wit
|
|
|
61
61
|
active: async () => connection,
|
|
62
62
|
resolve: async () => connection ? { type: 'key', key: 'byok' } : undefined,
|
|
63
63
|
} } }, { cloudClient });
|
|
64
|
-
await assert.rejects(handlers.shipAccount({}, rpcCall(new AbortController().signal)), /active Carthagent
|
|
64
|
+
await assert.rejects(handlers.shipAccount({}, rpcCall(new AbortController().signal)), /active Carthagent Cloud OAuth/);
|
|
65
65
|
}
|
|
66
66
|
const handlers = createHandlers({ integration: { connection: { active: async () => undefined, resolve: async () => undefined } } }, { cloudClient });
|
|
67
67
|
const controller = new AbortController();
|
package/tests/tui.test.mjs
CHANGED
|
@@ -36,7 +36,7 @@ test('Cloud managed-usage exhaustion is detected only for failed Cloud assistant
|
|
|
36
36
|
assert.equal(isCloudCreditExhaustion({ type: 'message_end', message: { role: 'assistant', stopReason: 'error', errorMessage: 'credit is exhausted' } }, 'experiential-labs/model'), true);
|
|
37
37
|
assert.match(cloudManagedUsageNotice({ type: 'message_end', message: { role: 'assistant', provider: 'experiential-labs', stopReason: 'error', errorMessage: '429: {"error":{"code":"daily_limit_reached"}}' } }), /00:00 UTC/);
|
|
38
38
|
assert.match(cloudManagedUsageNotice({ type: 'message_end', message: { role: 'assistant', provider: 'experiential-labs', stopReason: 'error', errorMessage: 'monthly_allowance_exhausted' } }), /billing portal/);
|
|
39
|
-
assert.match(cloudManagedUsageNotice({ type: 'message_end', message: { role: 'assistant', provider: 'experiential-labs', stopReason: 'error', errorMessage: 'subscription_inactive' } }), /
|
|
39
|
+
assert.match(cloudManagedUsageNotice({ type: 'message_end', message: { role: 'assistant', provider: 'experiential-labs', stopReason: 'error', errorMessage: 'subscription_inactive' } }), /awaiting payment or activation/);
|
|
40
40
|
assert.equal(isCloudCreditExhaustion({ type: 'message_end', message: { role: 'assistant', provider: 'openai', stopReason: 'error', errorMessage: 'daily_limit_reached' } }), false);
|
|
41
41
|
assert.equal(isCloudCreditExhaustion({ type: 'message_end', message: { role: 'assistant', provider: 'experiential-labs', stopReason: 'stop' } }), false);
|
|
42
42
|
});
|
|
@@ -69,7 +69,7 @@ test('active model synchronization refreshes welcome blocks without redundant in
|
|
|
69
69
|
|
|
70
70
|
syncActiveModel(state, feed, { provider: 'experiential-labs', id: 'gpt-6-luna' });
|
|
71
71
|
assert.equal(state.model, 'experiential-labs/gpt-6-luna');
|
|
72
|
-
assert.equal(feed.blocks[0].model, 'Carthagent
|
|
72
|
+
assert.equal(feed.blocks[0].model, 'Carthagent Cloud/gpt-6-luna');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
const SAMPLE_PLAN = {
|
|
@@ -28,7 +28,9 @@ export async function fixtureStopDiagnostics({ child, output, elapsedMs }, { run
|
|
|
28
28
|
? ['-NoProfile', '-NonInteractive', '-Command', `$p = Get-Process -Id ${child.pid} -ErrorAction SilentlyContinue; if ($null -ne $p) { Write-Output ("pid={0} responding={1}" -f $p.Id, $p.Responding) }`]
|
|
29
29
|
: ['-p', String(child.pid), '-o', 'pid=,ppid=,stat=,etime=,time='];
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
// Cold PowerShell startup on shared Windows runners can exceed two seconds.
|
|
32
|
+
// Keep diagnostics bounded without mistaking startup latency for no process.
|
|
33
|
+
const { stdout } = await run(command, args, { timeout: windows ? 10000 : 2000, maxBuffer: 4096, windowsHide: true });
|
|
32
34
|
const state = stdout.trim();
|
|
33
35
|
// Only numeric process metadata and fixed state codes; never argv, names,
|
|
34
36
|
// environment, stderr, database contents, or OS command error messages.
|
|
@@ -488,7 +488,13 @@ def main():
|
|
|
488
488
|
visible = screen.visible().lower()
|
|
489
489
|
visible_original = screen.visible()
|
|
490
490
|
if 'connection lost' in visible or 'reconnecting to the server' in visible:
|
|
491
|
-
|
|
491
|
+
# This isolated fixture is deleted on exit. Preserve its
|
|
492
|
+
# bounded diagnostics in the failure rather than losing the
|
|
493
|
+
# server-side reason with the temporary profile.
|
|
494
|
+
logs = {str(log.relative_to(root)): log.read_text(errors='replace')[-12000:]
|
|
495
|
+
for log in Path(root).rglob('*.log')}
|
|
496
|
+
raise RuntimeError(f'native host reconnect overlay during terminal acceptance: '
|
|
497
|
+
f'{visible_original!r}; fixture logs={logs!r}')
|
|
492
498
|
home_ready = 'shift+tab agents' in visible and 'ctrl+p commands' in visible
|
|
493
499
|
onboarding_visible = ('connect opencode go' in visible or
|
|
494
500
|
'connect an integration to' in visible or
|
|
@@ -230,8 +230,10 @@ try {
|
|
|
230
230
|
assert.ok(ship, 'the native provider catalog must supply a real Ship integration record');
|
|
231
231
|
assert.ok(ship.methods.some(({ type, id }) => type === 'oauth' && id === 'carthagent-device'),
|
|
232
232
|
'plugin integration transform must register the Ship device-OAuth method');
|
|
233
|
-
assert.
|
|
234
|
-
'
|
|
233
|
+
assert.equal(ship.methods.some(({ type }) => type === 'key'), false,
|
|
234
|
+
'fresh Cloud sign-in must hide the advanced API-key method');
|
|
235
|
+
assert.equal(ship.methods.find(({ type, id }) => type === 'oauth' && id === 'carthagent-device').form?.length ?? 0, 0,
|
|
236
|
+
'fresh Cloud sign-in must not prompt for legacy import');
|
|
235
237
|
assert.ok(ship.methods.some(({ type, names }) => type === 'env' && names?.includes('EXPLABS_API_KEY')),
|
|
236
238
|
'the native integration catalog resolves BYOK from the named environment credential');
|
|
237
239
|
const models = await client.model.list({ location: { directory: cwd } }, request);
|
|
@@ -123,7 +123,7 @@ try {
|
|
|
123
123
|
} while (Date.now() < deadline);
|
|
124
124
|
console.log(JSON.stringify({ modelRequests, models: models.map(model => ({ id: model.id, providerID: model.providerID, connected: model.connected, status: model.status })) }));
|
|
125
125
|
assert.ok(models.some(model => model.id === 'ctg-ship-fixture'), 'Successful Ship OAuth must publish discovered models in the real native model inventory without EXPLABS_API_KEY');
|
|
126
|
-
assert.equal(ship.name, 'Carthagent
|
|
126
|
+
assert.equal(ship.name, 'Carthagent Cloud', 'The connect picker must show the Cloud name');
|
|
127
127
|
assert.deepEqual(integrations.data.map(item => item.id).sort(), ['anthropic', 'experiential-labs', 'google', 'openai', 'xai'], 'Login must contain only the curated major integrations');
|
|
128
128
|
const providers = (await client.provider.list({ location }, request)).data;
|
|
129
129
|
assert.ok(!providers.some(provider => /^opencode(?:-|$)/.test(provider.id)), 'The upstream provider must not be available');
|