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.
@@ -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.
@@ -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 Ship requires fetch support.');
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 'Managed daily usage is exhausted. It resets at 00:00 UTC. Use /account for headroom or /login for BYOK.';
188
- if (/monthly_allowance_exhausted|managed monthly allowance/i.test(text)) return 'Managed monthly usage is exhausted. Use /billing portal to manage the plan or /login for BYOK.';
189
- if (/subscription_inactive|managed subscription is not active/i.test(text)) return 'The managed subscription is inactive. Use /billing portal or /login for BYOK.';
190
- if (/insufficient_quota|insufficient.credit|credit (?:is )?exhausted/i.test(text)) return 'Carthagent Ship credit is exhausted. Use /billing checkout where available or /login for BYOK.';
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 Ship account ${account?.accountId || 'unknown'}`,
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})` : 'Free'}`,
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 Ship 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.';
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 Ship is not connected. Run `ctg login` and choose Carthagent Ship, then retry.';
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 Ship is not connected. Run `ctg login` and choose Carthagent Ship, then retry.';
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 Ship is recommended), or set an API key env var (e.g. ANTHROPIC_API_KEY), then run `ctg native --list-models` again.');
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
- io.log(`Connected to ${integration.name}.`);
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
- io.log(`Connected to ${integration.name}.`);
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
- io.log(`Connected to ${integration.name}.`);
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') { io.log(`Connected to ${integration.name}.`); completed = true; return 0; }
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 Ship: ${error.message}`); }
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 { result = await auth.rpc.shipBilling({ action }, { location: { directory: cwd } }); }
949
- catch (error) { throw new Error(shipLoginGuidance(error) || `Carthagent Ship: ${error.message}`); }
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 Ship logout: ${error.message}`); }
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 Ship session ${sessionID}.` : 'Revoked and removed the current Carthagent Ship session.');
980
+ io.log(sessionID ? `Revoked Cloud session ${sessionID}.` : 'Revoked and removed the current Carthagent Cloud session.');
964
981
  return 0;
965
982
  }
966
983
 
@@ -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 Ship session; provider credentials are managed by the native runtime.', 1, io);
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
- if (!['checkout', 'portal'].includes(action)) return fail('Usage: ctg billing [checkout|portal]', 1, io);
200
- try { return Number((await ensureMethod(runtime, 'billing')({ action })) ?? 0); }
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 Ship', aliases: ['ship'], oauth: 'carthagent-device' }),
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.1",
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": "e0abac9f7e9d99f1b3b88d11e1efb7ce1de08b7e7474de33c1279ec1797d1aae",
13
- "executableSha256": "f6dd022de1854d67056b1e8b039196dcb69f887e5fe5dc8839e75d40debb9cc1"
12
+ "archiveSha256": "0389612a88f9b002b819e4fc12eb5bd9e337b9c5352de60dd3f523e343ac01c2",
13
+ "executableSha256": "8a34735274c0b62049f6ae882dc955016fde54458efc18822fbcedb2e8eddeda"
14
14
  },
15
15
  "linux-arm64": {
16
16
  "name": "opencode-linux-arm64.tar.gz",
17
- "archiveSha256": "cf2e5b020b100801e817d650b8f57ba82118036ffb4f0296b29af4a9ce9c5afd",
18
- "executableSha256": "e494c40d16934576338ea8ddd7ad5a4935fc0dc8e1efcebd78eb8074af2a795f"
17
+ "archiveSha256": "7a01012e830a2bee2195402bf0c525bd345754ee4abb72af3ba4a597ceb096e5",
18
+ "executableSha256": "aea44aa7d12ee9aef5e6fd21fbfe1d23bd2aa2c731d955d5393c4fe2c4b1dcd7"
19
19
  },
20
20
  "darwin-x64": {
21
21
  "name": "opencode-darwin-x64.tar.gz",
22
- "archiveSha256": "ed5f5cfa0edeca391ecaf7789347916c3d1779b9b1c8afb6feafb6a1fa82e95c",
23
- "executableSha256": "eddb5ac76384986a05cd86aecf4abe6fb76b11f0370775a515b46e07a6875e9c"
22
+ "archiveSha256": "cf88ba7c06a75a9e2021cfbb783a23fb93f09e2fb2dce86067346c8c666a2c5b",
23
+ "executableSha256": "17c67ad68d84a9c489a4d3567784aa8a2e3f7b086153693a57c1ef0a8b270a4f"
24
24
  },
25
25
  "darwin-arm64": {
26
26
  "name": "opencode-darwin-arm64.tar.gz",
27
- "archiveSha256": "f3ee781431819b5d1a5c35905d88d56cef0ead5b96b7007da6e046e531a6a87d",
28
- "executableSha256": "3afa14710324db7db3598d51c7616405615d78c3804fd77faf82b424747561fa"
27
+ "archiveSha256": "8db89fcec1f4fe502dfbb6f0e9ad6e2ccecdc6191ad14d6afa5338e9e0990341",
28
+ "executableSha256": "789dc613d191d339ef4b789d36dd32403db9422f11841659aea5a9ec4d3e6e37"
29
29
  },
30
30
  "windows-x64": {
31
31
  "name": "opencode-windows-x64.tar.gz",
32
- "archiveSha256": "8a433fa26fccc04e010009f9ffe368c322d867a3030cb3bb831bca3e23c328f1",
33
- "executableSha256": "139325dd6b5d9535b0259dec236b6493671909c6f61ed9ae7772b80b41332037"
32
+ "archiveSha256": "9f7139dea76e662109a4c80b109b92525c993a37c69aae5b45623f8d69ab9467",
33
+ "executableSha256": "38edbca2f4304f2efa2afa57b654c362c14f422c141c3ebd82e1062221dccd80"
34
34
  }
35
35
  }
36
36
  }
@@ -1,5 +1,5 @@
1
1
  export const CARTHAGENT_SHIP_PROVIDER_ID = 'experiential-labs';
2
- export const CARTHAGENT_SHIP_PROVIDER_NAME = 'Carthagent Ship';
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 || '');
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carthagent",
3
- "version": "1.1.0-beta.1",
3
+ "version": "1.1.0-beta.2",
4
4
  "workspaces": [
5
5
  "plugins/carthagent"
6
6
  ],
@@ -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' } }, required: ['url'], additionalProperties: false },
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: 'Sign in with Carthagent Ship',
124
- form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy Ship credential instead of signing in?',
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 Ship credential was found and will be migrated into OpenCode integration storage.',
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: `Approve device code ${response.userCode} in your browser.`,
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 Ship',
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
- editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Use an Experiential API key' } });
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 Ship model discovery failed (${response.status}).`);
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 Ship OAuth connection is required.');
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 Ship OAuth connection is required.');
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 Ship OAuth connection is required.', {}); }
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
- if (!Number.isSafeInteger(requested) || requested < 1 || requested > 200) throw new Error('Ship usage limit must be between 1 and 200.');
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 Ship OAuth connection is required.', {}); }
45
- const action = (input as { action: 'checkout' | 'portal' }).action;
46
- if (action !== 'checkout' && action !== 'portal') throw new Error('Unsupported Ship billing action.');
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, successUrl: returnUrl, cancelUrl: returnUrl, signal: call.signal });
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 Ship OAuth connection is required.', {}); }
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 Ship account", message: cloudAccountText(result.account) });
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 Ship usage", message: cloudUsageText(usageResult.usage, accountResult.account) });
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() || "checkout";
324
- if (!["checkout", "portal"].includes(action)) { toast("Usage: /billing [checkout|portal].", "warning"); return; }
325
- const { url } = await shipClient(context, location).billing({ action: action as "checkout" | "portal" }) as { url: string };
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" ? "Ship billing portal" : "Ship checkout", message: `${opened ? "Opened in your browser." : "Open this URL in your browser:"}\n${url}` });
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 Ship session" : "Sign out of Carthagent Ship", message: session ? `Revoke Ship session ${session}?` : "Revoke the active Ship session and remove its Carthagent connection?" });
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 Ship session ${session}.` : "Revoked and removed the active Carthagent Ship connection.", "success");
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 Ship account, credits, plan, and sessions" },
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 Ship session or sign out this profile" },
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 Ship device sign-in; not grading consent", value: "signin" },
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 Ship device sign-in", value: "signin" },
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: Free[\s\S]*1 active, 1 revoked/);
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' }), /subscription is 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], /subscription is inactive/);
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 for BYOK/.test(message)));
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 Ship');
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 Ship');
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 Ship');
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/account'));
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 Ship OAuth connection is required.' });
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 Ship/i);
1015
- await assert.rejects(runtime.logout({}), /not connected.*ctg login.*Carthagent Ship/i);
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', '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][1].method.type, 'key');
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}/account`, cancelUrl: `${baseUrl}/account` });
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 Ship OAuth/);
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();
@@ -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' } }), /subscription is 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 Ship/gpt-6-luna');
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
- const { stdout } = await run(command, args, { timeout: 2000, maxBuffer: 4096, windowsHide: true });
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
- raise RuntimeError(f'native host reconnect overlay during terminal acceptance: {visible_original!r}')
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.ok(ship.methods.some(({ type }) => type === 'key'),
234
- 'plugin integration transform must register the Ship API-key method');
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 Ship', 'The connect picker must show the Ship name');
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');