carthagent 1.1.0-beta.1 → 1.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cloud-onboarding-acceptance.md +65 -0
- package/docs/release-notes-beta.2.md +60 -0
- package/lib/cloud/client.mjs +13 -7
- package/lib/opencode/cli-runtime.mjs +34 -17
- package/lib/opencode/cli.mjs +4 -3
- 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/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 +2 -2
- 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,65 @@
|
|
|
1
|
+
# Cloud onboarding — unreleased implementation
|
|
2
|
+
|
|
3
|
+
November 2, 2026. This branch is not published beta acceptance. npm `next` remains
|
|
4
|
+
1.1.0-beta.1 and `latest` remains 1.0.7.
|
|
5
|
+
|
|
6
|
+
Native PR #238 merged at `a5ad2164d056fee7f7183579ce6cc97305bae53b` after CI
|
|
7
|
+
41504674623 passed. First-task guidance is tracked in PR #239. Beta.2 runtime
|
|
8
|
+
prebuild 41505735348 succeeded. All five downloaded archives, executable digests
|
|
9
|
+
and pinned provenance were independently verified by regenerating the candidate
|
|
10
|
+
manifest locally. Package and lockfile target beta.2 and pin that exact manifest;
|
|
11
|
+
publication remains pending.
|
|
12
|
+
|
|
13
|
+
## Changes
|
|
14
|
+
|
|
15
|
+
- Use Carthagent Cloud for hosted models; retain Ship mode, internal IDs and
|
|
16
|
+
compatibility login aliases.
|
|
17
|
+
- Fresh device sign-in has one primary flow. Legacy import and upstream key
|
|
18
|
+
configuration require `CARTHAGENT_ADVANCED_AUTH=1`.
|
|
19
|
+
- Billing selects Builder/Pro/Max and retrieves authenticated server quote terms.
|
|
20
|
+
TUI review precedes opening checkout. Existing subscribers review exact Stripe
|
|
21
|
+
proration in the browser before confirming a change.
|
|
22
|
+
- `/billing refresh` and `ctg billing refresh` show current account access and
|
|
23
|
+
require explicit continuation in the existing task session.
|
|
24
|
+
- Cloud account text identifies the signed-in email; usage recovery distinguishes
|
|
25
|
+
starter credit, daily/monthly limits, billing and own-provider access.
|
|
26
|
+
|
|
27
|
+
## Verification
|
|
28
|
+
|
|
29
|
+
- Full native suite: 1,131 passed, 5 skipped, 0 failed.
|
|
30
|
+
`/tmp/opencode/onboarding-native-reviewed.log`
|
|
31
|
+
- Native build and typecheck pass.
|
|
32
|
+
`/tmp/opencode/onboarding-native-reviewed-build.log`
|
|
33
|
+
- RPC regression verifies that eligibility comes from the authenticated server
|
|
34
|
+
and quoting does not create a checkout.
|
|
35
|
+
- Native host acceptance passes at `b715ff282b` after correcting stale fresh-auth
|
|
36
|
+
and Cloud-name expectations. Both CI installed-package jobs passed. Run
|
|
37
|
+
41502591162 passed Linux/macOS and Windows 24; Windows 22 hit the two-second
|
|
38
|
+
PowerShell diagnostics timeout, while its parallel run passed. Windows
|
|
39
|
+
diagnostics now allow bounded ten-second cold startup.
|
|
40
|
+
- Installed package acceptance and its focused terminal rerun reproduced a
|
|
41
|
+
reconnect overlay. DEBUG-enabled source and installed probes passed; a normal
|
|
42
|
+
source run subsequently hit its existing terminal-snapshot deadline. This is
|
|
43
|
+
timing-sensitive local acceptance, not proof of a fixed runtime defect. Both
|
|
44
|
+
earlier CI package checks passed, and corrected CI passed before merge.
|
|
45
|
+
- The subsequent full non-focused installed-package run passed all 28
|
|
46
|
+
probes/scenarios, including terminal semantics, using the package-pinned
|
|
47
|
+
beta.1 owned runtime (`/tmp/opencode/onboarding-native-package-final.log`).
|
|
48
|
+
This closes the local rerun gate for the onboarding source; beta.2 still
|
|
49
|
+
requires acceptance against its own final packaged manifest and release assets.
|
|
50
|
+
|
|
51
|
+
## Outstanding gates
|
|
52
|
+
|
|
53
|
+
Backend counterpart: `B-icy/Carthagent-Backend` PR #178 merged at
|
|
54
|
+
`b0eacdcbdb71be4c099b1b3839a11164b2d69232`; production deployment 41502378359
|
|
55
|
+
succeeded. Its `ONBOARDING_CONVERSION_AUDIT.md` tracks the full matrix. Live
|
|
56
|
+
authenticated identity and read-only Builder/Pro/Max handoff terms were verified.
|
|
57
|
+
Retention follow-up PR #179 also merged and deployment 41505976672 succeeded.
|
|
58
|
+
Stripe test access is unavailable. First-task/default-model guidance,
|
|
59
|
+
contextual exhaustion action, remaining legacy/docs naming and full measurement
|
|
60
|
+
segmentation still need work. No new end-to-end paid continuation or visual
|
|
61
|
+
screenshot acceptance is claimed.
|
|
62
|
+
|
|
63
|
+
The deployed website now precedes the native release. Publish a new immutable
|
|
64
|
+
beta and verify its exact public acquisition and upgrade. Preserve the user's
|
|
65
|
+
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.
|
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}`);
|
|
@@ -101,7 +101,7 @@ 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
103
|
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
|
|
104
|
+
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
105
|
}
|
|
106
106
|
if (raw) return `Native run failed: ${raw}`;
|
|
107
107
|
if (latest?.reason || latest?.budgetReason) return `Native run failed: ${latest.reason ?? latest.budgetReason}`;
|
|
@@ -311,10 +311,10 @@ function shipLoginGuidance(error) {
|
|
|
311
311
|
const message = String(error?.data?.message || error?.message || '');
|
|
312
312
|
const type = String(error?.type || error?.data?.type || '');
|
|
313
313
|
if (type === 'ship.unauthenticated' || type === 'ship.not-connected') {
|
|
314
|
-
return 'Carthagent
|
|
314
|
+
return 'Carthagent Cloud is not connected. Run `ctg login` and choose Carthagent Cloud, then retry.';
|
|
315
315
|
}
|
|
316
|
-
if (/active Carthagent Ship OAuth connection is required|not authenticated|unauthorized/i.test(message)) {
|
|
317
|
-
return 'Carthagent
|
|
316
|
+
if (/active Carthagent (?:Ship|Cloud) OAuth connection is required|not authenticated|unauthorized/i.test(message)) {
|
|
317
|
+
return 'Carthagent Cloud is not connected. Run `ctg login` and choose Carthagent Cloud, then retry.';
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
@@ -720,7 +720,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
720
720
|
if (!rows.length) {
|
|
721
721
|
io.error(term
|
|
722
722
|
? `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
|
|
723
|
+
: '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
724
|
}
|
|
725
725
|
return rows.length;
|
|
726
726
|
}
|
|
@@ -741,6 +741,12 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
741
741
|
let integration;
|
|
742
742
|
let attempt;
|
|
743
743
|
let completed = false;
|
|
744
|
+
const connected = () => {
|
|
745
|
+
completed = true;
|
|
746
|
+
io.log(`Connected to ${integration.name}.`);
|
|
747
|
+
io.log('Run ctg in your project. Start small: “Find one failing test, explain the cause, and fix it with a regression test.”');
|
|
748
|
+
if (integration.id === 'experiential-labs') io.log('Use /account to check the connected account and available usage. Starter usage is granted only when eligible.');
|
|
749
|
+
};
|
|
744
750
|
try {
|
|
745
751
|
({ client } = await serviceClient());
|
|
746
752
|
location = { directory: cwd };
|
|
@@ -809,7 +815,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
809
815
|
}
|
|
810
816
|
if (method.type === 'external') {
|
|
811
817
|
await client.integration.connect.external({ integrationID: integration.id, methodID: method.id, answer, location }, { signal: controller.signal });
|
|
812
|
-
|
|
818
|
+
connected();
|
|
813
819
|
return 0;
|
|
814
820
|
}
|
|
815
821
|
if (method.type === 'key') {
|
|
@@ -817,8 +823,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
817
823
|
const key = secretField ? answer[secretField.key] : await questionMasked(stdin, stdout, `${integration.name} API key`, controller.signal);
|
|
818
824
|
if (typeof key !== 'string' || !key.trim()) throw new Error('An API key is required.');
|
|
819
825
|
await client.integration.connect.key({ integrationID: integration.id, key, answer, location }, { signal: controller.signal });
|
|
820
|
-
|
|
821
|
-
completed = true;
|
|
826
|
+
connected();
|
|
822
827
|
return 0;
|
|
823
828
|
}
|
|
824
829
|
if (method.type !== 'oauth') throw new Error(`Authentication method ${method.type} is not supported by native CLI login.`);
|
|
@@ -838,15 +843,14 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
838
843
|
code = await questionWithAbort(prompt, 'Paste the authorization code: ', controller.signal);
|
|
839
844
|
if (!code.trim()) throw new Error('Authorization code is required.');
|
|
840
845
|
await client.integration.oauth.complete({ integrationID: integration.id, attemptID: attempt.attemptID, code: code.trim(), location }, { signal: controller.signal });
|
|
841
|
-
|
|
842
|
-
completed = true;
|
|
846
|
+
connected();
|
|
843
847
|
return 0;
|
|
844
848
|
}
|
|
845
849
|
io.log('Waiting for authorization… (Ctrl+C to cancel)');
|
|
846
850
|
while (!controller.signal.aborted && Date.now() < attempt.time.expires) {
|
|
847
851
|
const response = await client.integration.oauth.status({ integrationID: integration.id, attemptID: attempt.attemptID, location }, { signal: controller.signal });
|
|
848
852
|
const status = response.data;
|
|
849
|
-
if (status.status === 'complete') {
|
|
853
|
+
if (status.status === 'complete') { connected(); return 0; }
|
|
850
854
|
if (status.status === 'failed') throw new Error(status.message || 'Authorization failed.');
|
|
851
855
|
if (status.status === 'expired') throw new Error('Authorization expired.');
|
|
852
856
|
await new Promise((resolve, reject) => {
|
|
@@ -929,7 +933,7 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
929
933
|
const auth = await shipCredential();
|
|
930
934
|
let result;
|
|
931
935
|
try { result = (await auth.rpc.shipAccount({}, { location: { directory: cwd } })).account; }
|
|
932
|
-
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent
|
|
936
|
+
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Cloud: ${error.message}`); }
|
|
933
937
|
const cloud = await cloudClient('');
|
|
934
938
|
if (args.includes('--json')) io.log(JSON.stringify(result));
|
|
935
939
|
else {
|
|
@@ -942,13 +946,26 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
942
946
|
return 0;
|
|
943
947
|
}
|
|
944
948
|
|
|
945
|
-
async function billing({ action }) {
|
|
949
|
+
async function billing({ action, plan }) {
|
|
950
|
+
if (action === 'refresh') {
|
|
951
|
+
const result = await account();
|
|
952
|
+
io.log('Return to your existing session. When usage is available, explicitly continue the task.');
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
946
955
|
const auth = await shipCredential();
|
|
947
956
|
let result;
|
|
948
|
-
try {
|
|
949
|
-
|
|
957
|
+
try {
|
|
958
|
+
if (action === 'checkout') {
|
|
959
|
+
const { quote } = await auth.rpc.shipBilling({ action: 'quote', ...(plan ? { plan } : {}) }, { location: { directory: cwd } });
|
|
960
|
+
io.log(quote.terms);
|
|
961
|
+
if (quote.changingPlan) io.log('Review the exact proration in your browser before confirming the change.');
|
|
962
|
+
}
|
|
963
|
+
result = await auth.rpc.shipBilling({ action, ...(plan ? { plan } : {}) }, { location: { directory: cwd } });
|
|
964
|
+
}
|
|
965
|
+
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Cloud: ${error.message}`); }
|
|
950
966
|
const opened = await openBrowserImpl(result.url).catch(() => false);
|
|
951
967
|
io.log(`${opened ? 'Opened' : 'Open'} ${result.url}`);
|
|
968
|
+
io.log('After payment, run ctg billing refresh. Return to your existing session and explicitly continue the task.');
|
|
952
969
|
return 0;
|
|
953
970
|
}
|
|
954
971
|
|
|
@@ -956,11 +973,11 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
956
973
|
const auth = await shipCredential();
|
|
957
974
|
let result;
|
|
958
975
|
try { result = await auth.rpc.shipLogout({ ...(sessionID ? { session: sessionID } : {}) }, { location: { directory: cwd } }); }
|
|
959
|
-
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent
|
|
976
|
+
catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Cloud logout: ${error.message}`); }
|
|
960
977
|
if (!sessionID && result.credentialID) {
|
|
961
978
|
await auth.client.credential.remove({ credentialID: result.credentialID });
|
|
962
979
|
}
|
|
963
|
-
io.log(sessionID ? `Revoked
|
|
980
|
+
io.log(sessionID ? `Revoked Cloud session ${sessionID}.` : 'Revoked and removed the current Carthagent Cloud session.');
|
|
964
981
|
return 0;
|
|
965
982
|
}
|
|
966
983
|
|
package/lib/opencode/cli.mjs
CHANGED
|
@@ -189,15 +189,16 @@ export async function runOpenCodeCli({ argv = [], runtime, io = console, stdoutI
|
|
|
189
189
|
return fail(error.message, 1, io);
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
if (normalized === 'logout') return fail('Use `logout-cloud` to revoke a Carthagent
|
|
192
|
+
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
193
|
if (normalized === 'account' || normalized === 'cloud') {
|
|
194
194
|
try { return Number((await ensureMethod(runtime, 'account')({ args: rest })) ?? 0); }
|
|
195
195
|
catch (error) { return fail(error.message, 1, io); }
|
|
196
196
|
}
|
|
197
197
|
if (normalized === 'billing') {
|
|
198
198
|
const action = rest[0] || 'checkout';
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const plan = rest[1];
|
|
200
|
+
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);
|
|
201
|
+
try { return Number((await ensureMethod(runtime, 'billing')({ action, ...(plan ? { plan } : {}) })) ?? 0); }
|
|
201
202
|
catch (error) { return fail(error.message, 1, io); }
|
|
202
203
|
}
|
|
203
204
|
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.2",
|
|
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": "0389612a88f9b002b819e4fc12eb5bd9e337b9c5352de60dd3f523e343ac01c2",
|
|
13
|
+
"executableSha256": "8a34735274c0b62049f6ae882dc955016fde54458efc18822fbcedb2e8eddeda"
|
|
14
14
|
},
|
|
15
15
|
"linux-arm64": {
|
|
16
16
|
"name": "opencode-linux-arm64.tar.gz",
|
|
17
|
-
"archiveSha256": "
|
|
18
|
-
"executableSha256": "
|
|
17
|
+
"archiveSha256": "7a01012e830a2bee2195402bf0c525bd345754ee4abb72af3ba4a597ceb096e5",
|
|
18
|
+
"executableSha256": "aea44aa7d12ee9aef5e6fd21fbfe1d23bd2aa2c731d955d5393c4fe2c4b1dcd7"
|
|
19
19
|
},
|
|
20
20
|
"darwin-x64": {
|
|
21
21
|
"name": "opencode-darwin-x64.tar.gz",
|
|
22
|
-
"archiveSha256": "
|
|
23
|
-
"executableSha256": "
|
|
22
|
+
"archiveSha256": "cf88ba7c06a75a9e2021cfbb783a23fb93f09e2fb2dce86067346c8c666a2c5b",
|
|
23
|
+
"executableSha256": "17c67ad68d84a9c489a4d3567784aa8a2e3f7b086153693a57c1ef0a8b270a4f"
|
|
24
24
|
},
|
|
25
25
|
"darwin-arm64": {
|
|
26
26
|
"name": "opencode-darwin-arm64.tar.gz",
|
|
27
|
-
"archiveSha256": "
|
|
28
|
-
"executableSha256": "
|
|
27
|
+
"archiveSha256": "8db89fcec1f4fe502dfbb6f0e9ad6e2ccecdc6191ad14d6afa5338e9e0990341",
|
|
28
|
+
"executableSha256": "789dc613d191d339ef4b789d36dd32403db9422f11841659aea5a9ec4d3e6e37"
|
|
29
29
|
},
|
|
30
30
|
"windows-x64": {
|
|
31
31
|
"name": "opencode-windows-x64.tar.gz",
|
|
32
|
-
"archiveSha256": "
|
|
33
|
-
"executableSha256": "
|
|
32
|
+
"archiveSha256": "9f7139dea76e662109a4c80b109b92525c993a37c69aae5b45623f8d69ab9467",
|
|
33
|
+
"executableSha256": "38edbca2f4304f2efa2afa57b654c362c14f422c141c3ebd82e1062221dccd80"
|
|
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
|
];
|
|
@@ -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();
|
|
@@ -1011,8 +1011,8 @@ test('native Ship billing and logout explain how to connect when the typed auth
|
|
|
1011
1011
|
shipLogout: async () => { throw Object.assign(new Error('Not authenticated'), { type: 'ship.unauthenticated' }); },
|
|
1012
1012
|
}) }),
|
|
1013
1013
|
} });
|
|
1014
|
-
await assert.rejects(runtime.billing({ action: 'checkout' }), /not connected.*ctg login.*Carthagent
|
|
1015
|
-
await assert.rejects(runtime.logout({}), /not connected.*ctg login.*Carthagent
|
|
1014
|
+
await assert.rejects(runtime.billing({ action: 'checkout' }), /not connected.*ctg login.*Carthagent Cloud/i);
|
|
1015
|
+
await assert.rejects(runtime.logout({}), /not connected.*ctg login.*Carthagent Cloud/i);
|
|
1016
1016
|
});
|
|
1017
1017
|
|
|
1018
1018
|
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');
|