@byokit/openclaw 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.5 (2026-10-01)
6
+
7
+
8
+
9
+ - FIX: Failed engine starts no longer remove another live gateway’s guards or saved sign-in state. Verified dead-host gateways stop gracefully before restart; ambiguous ownership returns a typed engine-already-running error and preserves the store.
10
+
11
+ ## 0.3.4 (2026-09-30)
12
+
13
+
14
+
15
+ - FIX: Device-code sign-in waits for the engine’s code expiry instead of failing after two minutes; caller cancellation and expiry return typed outcomes with plain words.
16
+ - Offer the native Claude Code / Agent SDK subscription route (`claude-cli`, plugin `anthropic`), keeping login in Claude Code in the isolated engine HOME. Sign-in uses engine detection and live activation; `oc.state` reports native login readiness.
17
+ - Support explicit Anthropic API key activation with a per-use billing label and secret-safe views; never offer an API-key fallback. Direct Claude.ai OAuth and setup-token offers remain excluded.
18
+ - Verify native and API per-run provider/model forwarding, tool events and strict refusal of `@profile` pins.
19
+
5
20
  ## 0.3.3 (2026-09-30)
6
21
 
7
22
  - Dependency update: pins @byokit/secrets 0.5.0.
package/README.md CHANGED
@@ -33,21 +33,36 @@ the pinned engine and needs a real ChatGPT sign-in, so it is not run here):
33
33
  ```ts
34
34
  import { OpenClawKit } from '@byokit/openclaw';
35
35
 
36
- const kit = new OpenClawKit({ stateDir: './openclaw-state' });
36
+ const kit = new OpenClawKit({ stateDir: './openclaw-state', config: { plugins: { allow: ['openai'] } } });
37
37
  await kit.start(); // installs the pinned engine under stateDir, spawns it, connects
38
38
 
39
- await kit.ensureMember('ana');
40
- const signIn = kit.signIn('ana', { authChoice: 'openai-device-code', via: 'code' }, (view) => console.log(view));
41
- await signIn.done;
39
+ await kit.ensureMember('umer');
40
+ const controller = new AbortController(); // call controller.abort() when Umer cancels
41
+ const signIn = kit.signIn('umer', {
42
+ authChoice: 'openai-device-code', via: 'code', signal: controller.signal,
43
+ }, (view) => { if (view.code) console.log('Open', view.url, 'and enter', view.code); });
44
+ const signedIn = await signIn.done;
45
+ if (signedIn.state !== 'done') {
46
+ console.log(signedIn.error); // plain words; why is 'expired' or 'declined' on expiry or cancellation
47
+ await kit.stop();
48
+ process.exit(0);
49
+ }
42
50
 
43
51
  const end = await kit.run(
44
- { member: 'ana', sessionKey: 'agent:ana:main', message: 'Say hello.' },
52
+ { member: 'umer', sessionKey: 'agent:umer:main', message: 'Say hello.' },
45
53
  (e) => { if (e.type === 'text') console.log(e.text); },
46
54
  );
47
55
  console.log(end);
48
56
  await kit.stop();
49
57
  ```
50
58
 
59
+ Device approval can take longer than two minutes. The kit waits through the engine's advertised code lifetime
60
+ (`expiresInMinutes` on the pin, or `expires_in` seconds when supplied), including progress pulls. If the engine
61
+ supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their 120-second timeout.
62
+ `signIn.cancel()` or the optional caller `signal` cancels this sign-in and releases its wizard session.
63
+ `done` returns a typed `SignInView`: `why: 'expired'` for an expired code, `why: 'declined'` for cancellation,
64
+ with a plain sentence in `error`. `SignInOptions` is exported from the host entry.
65
+
51
66
  To choose which of a member's accounts a run uses, pass `model: 'provider/model'` in the run spec (for example
52
67
  `'openai/gpt-5.1'`, where `provider` is an id `kit.providers(member)` lists). That provider is the one called and billed for this run only, with no fallback to another
53
68
  provider or model. If the member isn't signed in to it, the run ends `{ ok: false, kind: 'signed-out' }` and the
@@ -77,9 +92,9 @@ const kit = new OpenClawKit({ stateDir, spawnEngine: false, transport: fakeGatew
77
92
  try {
78
93
  await kit.start();
79
94
  console.log('state:', kit.state.phase, '| protocol:', kit.hello?.protocol);
80
- await kit.ensureMember('ana');
95
+ await kit.ensureMember('umer');
81
96
  const end = await kit.run(
82
- { member: 'ana', sessionKey: 'agent:ana:main', message: 'Say hello.' },
97
+ { member: 'umer', sessionKey: 'agent:umer:main', message: 'Say hello.' },
83
98
  (e) => console.log('event:', JSON.stringify(e)),
84
99
  );
85
100
  console.log('end:', JSON.stringify(end));
@@ -217,7 +232,14 @@ before any restoration; a wrong key, tampering, or a missing adapter rejects. Fi
217
232
  have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
218
233
  regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
219
234
  dangling and directory symlinks (including loops), sockets, FIFOs and devices are skipped.
220
- One kit owns the store at a time; a live owner or orphan gateway blocks preparation instead of racing its writes.
235
+ One kit owns the store at a time. After a host crash, preparation stops a verified same-user gateway
236
+ only when its host lock owner is dead, waits up to three seconds for exit, then seals and restores the
237
+ leftover state under the acquired lock. If process identity is ambiguous (including unavailable process
238
+ inspection or a rewritten command line), another host is alive, or shutdown times out, `prepare()` and
239
+ `start()` reject with exported `EngineAlreadyRunningError` (`code: 'engine-already-running'`). The kit reports
240
+ `{ phase: 'failed', why: 'engine-already-running' }`; show `stateWords(kit.state)` and retry after the other
241
+ session stops. Failed-start cleanup and `stop()` on an instance with no ownership preserve the other
242
+ writer's pid, lock, sealed snapshot and live state.
221
243
  Sealing copies the whole store through memory, so startup and stop cost grows with session history.
222
244
 
223
245
  The migration doctor temporarily opens the same store and reseals it even when import fails. Once the gateway
@@ -259,3 +281,5 @@ frozen (docs/runtime-kits.md §5).
259
281
  ## License
260
282
 
261
283
  Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](../../NOTICE).
284
+
285
+ Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing), with login kept in Claude Code under the kit’s isolated `HOME=<stateDir>/openclaw/home` and `CLAUDE_CONFIG_DIR=<stateDir>/openclaw/home/.claude`; sign in there with `claude auth login`, then call `signIn`. Activation live-tests the route; native sign-out is through Claude Code. `apiKey` is explicit API billing (“API key (billed per use)”, `offer: false`): `signIn` asks for the key through `paste`, then engine activation verifies it. The kit never imports Claude credentials or implements direct Claude.ai OAuth; [Anthropic’s terms](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use) apply.
@@ -0,0 +1,17 @@
1
+ import type { GatewayTransport } from './types.ts';
2
+ export type AuthStatus = {
3
+ providers?: (string | {
4
+ provider?: unknown;
5
+ status?: unknown;
6
+ profiles?: {
7
+ status?: unknown;
8
+ }[];
9
+ usage?: unknown;
10
+ })[];
11
+ unavailable?: {
12
+ message?: unknown;
13
+ };
14
+ };
15
+ /** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
16
+ * setup detection asks the CLI itself, without handing credentials to the kit. */
17
+ export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean): Promise<AuthStatus>;
@@ -0,0 +1,13 @@
1
+ /** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
2
+ * setup detection asks the CLI itself, without handing credentials to the kit. */
3
+ export async function authStatus(request, agentId, refresh = false, native = false) {
4
+ const status = await request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 });
5
+ if (!native)
6
+ return status;
7
+ const detected = await request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 }).catch(() => ({}));
8
+ const rows = (status.providers ?? []).filter((row) => (typeof row === 'string' ? row : row.provider) !== 'claude-cli');
9
+ if (detected.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
10
+ return { providers: [...rows, { provider: 'claude-cli', status: 'ok' }] };
11
+ }
12
+ return { ...status, providers: rows };
13
+ }
@@ -2,20 +2,10 @@
2
2
  // Seal the complete isolated stores, including journals, only after their writer has exited.
3
3
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
5
6
  const encoder = new TextEncoder();
6
7
  const archive = (name) => /^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
7
8
  || name.endsWith('.moved-to-engine');
8
- const live = (pid) => {
9
- if (!Number.isSafeInteger(pid) || pid < 1)
10
- return false;
11
- try {
12
- process.kill(pid, 0);
13
- return true;
14
- }
15
- catch (error) {
16
- return error.code !== 'ESRCH';
17
- }
18
- };
19
9
  function regular(path) {
20
10
  if (!lstatSync(path).isFile())
21
11
  throw new Error(`credential store requires regular files: ${path}`);
@@ -108,18 +98,16 @@ export class AuthStore {
108
98
  return;
109
99
  if (existsSync(this.file) && !this.o.seal)
110
100
  throw new Error('authSeal required for sealed credential store');
111
- if (!this.o.seal)
112
- return;
113
101
  // Never race a live gateway or another kit, including an orphan after its host crashed.
114
102
  const gateway = join(this.o.root, 'gateway.pid');
115
103
  if (existsSync(gateway) && live(Number(readFileSync(gateway, 'utf8'))))
116
- throw new Error('credential store is in use');
104
+ throw new EngineAlreadyRunningError();
117
105
  if (existsSync(this.lock)) {
118
106
  if (!lstatSync(this.lock).isDirectory() || lstatSync(this.lock).isSymbolicLink())
119
107
  throw new Error('invalid credential lock');
120
108
  const owner = join(this.lock, 'pid');
121
109
  if (!existsSync(owner) || live(Number(readFileSync(owner, 'utf8'))))
122
- throw new Error('credential store is in use');
110
+ throw new EngineAlreadyRunningError();
123
111
  // Only one stale-owner recovery can proceed. Recheck after claiming it so a stale reader
124
112
  // cannot remove the lock a newer owner has just acquired.
125
113
  const recovery = join(this.lock, 'recovery');
@@ -127,17 +115,36 @@ export class AuthStore {
127
115
  mkdirSync(recovery, { mode: 0o700 });
128
116
  }
129
117
  catch {
130
- throw new Error('credential store is in use');
118
+ throw new EngineAlreadyRunningError();
131
119
  }
132
120
  if (!existsSync(owner) || live(Number(readFileSync(owner, 'utf8')))) {
133
121
  rmSync(recovery, { recursive: true });
134
- throw new Error('credential store is in use');
122
+ throw new EngineAlreadyRunningError();
135
123
  }
136
124
  rmSync(this.lock, { recursive: true });
137
125
  }
138
- mkdirSync(this.lock, { mode: 0o700 });
139
- writeFileSync(join(this.lock, 'pid'), String(process.pid), { mode: 0o600, flag: 'wx' });
126
+ try {
127
+ mkdirSync(this.lock, { mode: 0o700 });
128
+ }
129
+ catch (error) {
130
+ if (error.code === 'EEXIST')
131
+ throw new EngineAlreadyRunningError();
132
+ throw error;
133
+ }
140
134
  this.owned = true;
135
+ try {
136
+ writeFileSync(join(this.lock, 'pid'), String(process.pid), { mode: 0o600, flag: 'wx' });
137
+ // Recheck under the acquired lock before removing a dead writer's guard.
138
+ if (existsSync(gateway)) {
139
+ if (live(Number(readFileSync(gateway, 'utf8'))))
140
+ throw new EngineAlreadyRunningError();
141
+ removeMarker(gateway);
142
+ }
143
+ }
144
+ catch (error) {
145
+ this.release();
146
+ throw error;
147
+ }
141
148
  }
142
149
  release() {
143
150
  if (!this.owned)
@@ -297,7 +304,8 @@ export class AuthStore {
297
304
  }
298
305
  stop() {
299
306
  return this.serial(async () => {
300
- this.acquire();
307
+ if (!this.owned)
308
+ return;
301
309
  await this.archives();
302
310
  await this.persist();
303
311
  this.active = false;
@@ -0,0 +1,6 @@
1
+ /** A live or unverifiable writer still owns the isolated engine state. Retry after it stops. */
2
+ export declare class EngineAlreadyRunningError extends Error {
3
+ readonly code = "engine-already-running";
4
+ constructor();
5
+ }
6
+ export declare function pidAlive(pid: number): boolean;
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /** A live or unverifiable writer still owns the isolated engine state. Retry after it stops. */
3
+ export class EngineAlreadyRunningError extends Error {
4
+ code = 'engine-already-running';
5
+ constructor() {
6
+ super('engine already running; credential store is in use');
7
+ this.name = 'EngineAlreadyRunningError';
8
+ }
9
+ }
10
+ export function pidAlive(pid) {
11
+ if (!Number.isSafeInteger(pid) || pid < 1)
12
+ return true; // An invalid guard is ambiguous, never stale.
13
+ try {
14
+ process.kill(pid, 0);
15
+ // An orphan can remain a zombie until its new parent reaps it; it has no writer left.
16
+ if (process.platform === 'linux') {
17
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
18
+ if (stat.slice(stat.lastIndexOf(')') + 2).startsWith('Z '))
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ return error.code !== 'ESRCH';
25
+ }
26
+ }
package/dist/engine.d.ts CHANGED
@@ -36,7 +36,8 @@ export declare class Engine {
36
36
  doctor(timeoutMs: number): {
37
37
  status: number | null;
38
38
  };
39
- private stalePid;
39
+ private verifiedOrphan;
40
+ private recoverOrphan;
40
41
  start(): Promise<{
41
42
  port: number;
42
43
  token: string;
@@ -45,6 +46,7 @@ export declare class Engine {
45
46
  private locked;
46
47
  private startOnce;
47
48
  private launch;
49
+ private removeOwnedPid;
48
50
  withAuthStore<T>(task: () => Promise<T>): Promise<T>;
49
51
  stop(): Promise<void>;
50
52
  }
package/dist/engine.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { randomBytes, generateKeyPairSync } from 'node:crypto';
3
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync, realpathSync, statSync } from 'node:fs';
4
4
  import { createServer } from 'node:net';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { setTimeout as delay } from 'node:timers/promises';
8
8
  import { AuthStore } from "./auth-store.js";
9
+ import { EngineAlreadyRunningError, pidAlive } from "./engine-status.js";
9
10
  import { ENGINE_VERSION } from "./constants.js";
10
11
  import { reconcileConfig } from "./config.js";
11
12
  import { writePlugin, resolveBridge } from "./bridge.js";
@@ -93,6 +94,7 @@ export class Engine {
93
94
  async prepareOnce() {
94
95
  if (!this.o.authSeal && existsSync(join(this.root, 'auth-store.sealed')))
95
96
  throw new Error('authSeal required for sealed credential store');
97
+ await this.recoverOrphan();
96
98
  for (const d of [this.root, ...(!this.o.authSeal ? [join(this.root, 'home'), join(this.root, 'state')] : []), join(this.root, 'tmp'), join(this.root, 'install-home'), join(this.root, 'npm-cache'), join(this.o.stateDir, 'logs'), this.dir])
97
99
  mkdirSync(d, { recursive: true, mode: 0o700 });
98
100
  try {
@@ -168,19 +170,67 @@ export class Engine {
168
170
  const result = spawnSync(process.execPath, [entry, 'doctor', '--fix', '--yes', '--non-interactive'], { cwd: env.HOME, env, timeout: timeoutMs, stdio: 'pipe' });
169
171
  return { status: result.status };
170
172
  }
171
- stalePid() {
173
+ verifiedOrphan(pid) {
174
+ if (process.platform !== 'linux' || !process.getuid)
175
+ return false;
176
+ try {
177
+ const proc = `/proc/${pid}`;
178
+ const cmd = readFileSync(`${proc}/cmdline`, 'utf8').split('\0');
179
+ const env = readFileSync(`${proc}/environ`, 'utf8').split('\0');
180
+ const home = join(this.root, 'home');
181
+ const owner = join(this.root, 'auth-store.lock', 'pid');
182
+ return statSync(proc).uid === process.getuid()
183
+ && realpathSync(`${proc}/exe`) === realpathSync(process.execPath)
184
+ && cmd.indexOf(this.entry) > 0 && cmd[cmd.indexOf(this.entry) + 1] === 'gateway'
185
+ && realpathSync(`${proc}/cwd`) === realpathSync(home)
186
+ && env.includes(`HOME=${home}`)
187
+ && env.includes(`OPENCLAW_STATE_DIR=${join(this.root, 'state')}`)
188
+ && env.includes(`OPENCLAW_CONFIG_PATH=${join(this.root, 'openclaw.json')}`)
189
+ && existsSync(owner) && !pidAlive(Number(readFileSync(owner, 'utf8')));
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ async recoverOrphan() {
196
+ if (this.child && this.child.exitCode === null && this.child.signalCode === null)
197
+ return;
172
198
  const path = join(this.root, 'gateway.pid');
173
199
  if (!existsSync(path))
174
200
  return;
175
201
  const pid = Number(readFileSync(path, 'utf8'));
176
- if (!Number.isSafeInteger(pid) || pid < 1)
177
- return;
202
+ if (!pidAlive(pid))
203
+ return; // AuthStore removes stale guards only under its lock.
204
+ const occupied = () => { this.state('failed', 'engine-already-running'); return new EngineAlreadyRunningError(); };
205
+ if (!this.verifiedOrphan(pid))
206
+ throw occupied();
207
+ // Verify a stable process identity again immediately before signalling this pid, never its group.
208
+ const identity = () => {
209
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
210
+ return stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]; // starttime, not a reusable pid alone
211
+ };
212
+ try {
213
+ const before = identity();
214
+ if (!before || !this.verifiedOrphan(pid) || before !== identity())
215
+ throw occupied();
216
+ }
217
+ catch {
218
+ if (!pidAlive(pid))
219
+ return;
220
+ throw occupied();
221
+ }
178
222
  try {
179
- const cmd = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
180
- if (cmd.includes(this.entry) && cmd.includes('gateway'))
181
- process.kill(-pid, 'SIGTERM');
223
+ process.kill(pid, 'SIGTERM');
224
+ }
225
+ catch (error) {
226
+ if (error.code !== 'ESRCH')
227
+ throw occupied();
182
228
  }
183
- catch { /* pid is gone or not ours */ }
229
+ for (let i = 0; i < 15 && pidAlive(pid); i++)
230
+ await delay(200);
231
+ if (pidAlive(pid))
232
+ throw occupied();
233
+ // The new store owner now seals crash leftovers under its lock before restoring them.
184
234
  }
185
235
  start() {
186
236
  if (this.starting)
@@ -215,7 +265,9 @@ export class Engine {
215
265
  return this.launch();
216
266
  }
217
267
  catch (error) {
218
- await this.authStore.stop();
268
+ // A launch failure after spawn still has a writer; the caller's stop must await its exit.
269
+ if (!this.child)
270
+ await this.authStore.stop();
219
271
  throw error;
220
272
  }
221
273
  }
@@ -229,7 +281,6 @@ export class Engine {
229
281
  return { port: this.port, token: this.token, identityPath };
230
282
  if (this.child && this.child.exitCode === null)
231
283
  return { port: this.port, token: this.token, identityPath };
232
- this.stalePid();
233
284
  this.state('starting');
234
285
  const { entry, env } = this.doctorContext();
235
286
  const fd = openSync(join(this.o.stateDir, 'logs', 'openclaw.log'), 'a', 0o600);
@@ -255,7 +306,7 @@ export class Engine {
255
306
  if (this.child !== child || this.stopping)
256
307
  return;
257
308
  this.child = undefined;
258
- rmSync(join(this.root, 'gateway.pid'), { force: true });
309
+ this.removeOwnedPid(child.pid);
259
310
  void (async () => {
260
311
  await this.authStore.stop();
261
312
  if (this.stopping)
@@ -275,6 +326,11 @@ export class Engine {
275
326
  });
276
327
  return { port: this.port, token: this.token, identityPath };
277
328
  }
329
+ removeOwnedPid(pid) {
330
+ const path = join(this.root, 'gateway.pid');
331
+ if (pid && existsSync(path) && readFileSync(path, 'utf8') === String(pid))
332
+ rmSync(path, { force: true });
333
+ }
278
334
  withAuthStore(task) { return this.authStore.offline(task); }
279
335
  async stop() {
280
336
  this.stopping = true;
@@ -299,10 +355,9 @@ export class Engine {
299
355
  if (child && child.exitCode === null && child.signalCode === null)
300
356
  throw new Error('engine did not stop; credential store still in use');
301
357
  this.child = undefined;
302
- rmSync(join(this.root, 'gateway.pid'), { force: true });
358
+ this.removeOwnedPid(child?.pid);
303
359
  if (!this.credentialsLocked)
304
360
  await this.authStore.stop();
305
- rmSync(this.bridgeSock, { force: true });
306
361
  this.state('stopped');
307
362
  }
308
363
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
2
- export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, RunUsage, SignInView, ToolHost, ToolSpec, } from './types.ts';
2
+ export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
3
3
  export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
4
4
  export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
5
+ export { EngineAlreadyRunningError } from './engine-status.ts';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@
3
3
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from "./constants.js";
4
4
  export { OpenClawKit } from "./kit.js";
5
5
  export { stateWords, toAccountView, words } from "./words.js";
6
+ export { EngineAlreadyRunningError } from "./engine-status.js";
package/dist/kit.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SealingAdapter } from '@byokit/secrets';
2
- import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, SignInView, ToolHost, ToolSpec } from './types.ts';
2
+ import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
3
3
  export type KitOptions = {
4
4
  stateDir: string;
5
5
  authSeal?: SealingAdapter;
@@ -75,10 +75,7 @@ export declare class OpenClawKit {
75
75
  routes(): Route[];
76
76
  providers(member: Member): Promise<string[]>;
77
77
  signedIn(member: Member, provider: string): Promise<boolean>;
78
- signIn(member: Member, o: {
79
- authChoice: string;
80
- via?: 'browser' | 'code';
81
- }, on: (v: SignInView) => void): {
78
+ signIn(member: Member, o: SignInOptions, on: (v: SignInView) => void): {
82
79
  paste(text: string): void;
83
80
  cancel(): void;
84
81
  done: Promise<SignInView>;
package/dist/kit.js CHANGED
@@ -503,6 +503,10 @@ export class OpenClawKit {
503
503
  if (transport && this.transport === transport)
504
504
  await this.disconnect();
505
505
  await this.engine.stop();
506
+ if (error instanceof Error && 'code' in error && error.code === 'engine-already-running') {
507
+ this.setState({ phase: 'failed', why: 'engine-already-running' });
508
+ throw error;
509
+ }
506
510
  if (needsUpdate)
507
511
  this.setState({ phase: 'needs-update', why: 'version' });
508
512
  if (!needsUpdate && !this.stopping)
package/dist/link.js CHANGED
@@ -9,6 +9,7 @@ import { reach } from '@byokit/reach';
9
9
  import { ENGINE_VERSION } from "./constants.js";
10
10
  import { b64urlDecode, sealNotice } from "./notices.js";
11
11
  import { routeFor } from "./routes.js";
12
+ import { authStatus } from "./auth-status.js";
12
13
  import { signedInProviders } from "./runs.js";
13
14
  import { stateWords, words } from "./words.js";
14
15
  const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events']);
@@ -99,7 +100,11 @@ export function openclawLink(kit, o) {
99
100
  const signIns = async (member) => {
100
101
  try {
101
102
  const { agentId } = await kit.ensureMember(member);
102
- const status = await kit.call('models.authStatus', { agentId }, { timeoutMs: 5_000 });
103
+ const status = await authStatus((method, params, options) => {
104
+ if (method === 'models.authStatus')
105
+ return kit.call(method, params, options);
106
+ return kit.call('openclaw.setup.detect', params, options);
107
+ }, agentId, false, true);
103
108
  return signedInProviders(status);
104
109
  }
105
110
  catch {
package/dist/routes.d.ts CHANGED
@@ -6,5 +6,5 @@ import type { Route } from './types.ts';
6
6
  * listed: install-catalog plugins (external providers needing their own install) are not contracted by the pin and
7
7
  * are deliberately absent. */
8
8
  export declare function routes(): Route[];
9
- /** The route an app should use: only an offered route counts, so an Anthropic fallback can never be picked. */
9
+ /** The route an app should use: only an offered route counts; API billing is never picked as a fallback. */
10
10
  export declare function routeFor(provider: string, via: 'browser' | 'code'): Route | undefined;
package/dist/routes.js CHANGED
@@ -10,7 +10,7 @@ const table = routesJson;
10
10
  export function routes() {
11
11
  return table;
12
12
  }
13
- /** The route an app should use: only an offered route counts, so an Anthropic fallback can never be picked. */
13
+ /** The route an app should use: only an offered route counts; API billing is never picked as a fallback. */
14
14
  export function routeFor(provider, via) {
15
15
  return table.find((route) => route.offer && route.provider === provider && route.via === via);
16
16
  }
package/dist/routes.json CHANGED
@@ -111,14 +111,17 @@
111
111
  },
112
112
  {
113
113
  "choice": "anthropic-cli",
114
- "provider": "anthropic",
114
+ "provider": "claude-cli",
115
115
  "plugin": "anthropic",
116
116
  "billing": "subscription",
117
117
  "via": "browser",
118
- "prerequisite": "the Claude CLI, signed in inside the engine's own folder",
119
- "offer": false,
120
- "reason": "Requires an explicit app opt-in; the pinned Gateway does not expose this plan route",
121
- "source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
118
+ "prerequisite": "Sign in through unmodified Claude Code on this machine using the kit's isolated HOME and CLAUDE_CONFIG_DIR; the login stays in Claude Code.",
119
+ "offer": true,
120
+ "reason": "Claude subscription via your own Claude Code login on this machine (Agent SDK runtime).",
121
+ "source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
122
+ "auth": "cli",
123
+ "terms": "allowed",
124
+ "termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
122
125
  },
123
126
  {
124
127
  "choice": "apiKey",
@@ -128,8 +131,11 @@
128
131
  "via": "browser",
129
132
  "prerequisite": null,
130
133
  "offer": false,
131
- "reason": "API key (billed per use); shown only when an app explicitly asks.",
132
- "source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
134
+ "reason": "API key (billed per use)",
135
+ "source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
136
+ "auth": "api_key",
137
+ "terms": "allowed",
138
+ "termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
133
139
  },
134
140
  {
135
141
  "choice": "clawrouter-api-key",
@@ -348,8 +354,11 @@
348
354
  "via": "browser",
349
355
  "prerequisite": null,
350
356
  "offer": false,
351
- "reason": "Requires an explicit app opt-in; the pinned Gateway does not expose this plan route",
352
- "source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
357
+ "reason": "Token import is not the native Claude Code login route; direct Claude.ai OAuth remains excluded.",
358
+ "source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
359
+ "auth": "token",
360
+ "terms": "grey",
361
+ "termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
353
362
  },
354
363
  {
355
364
  "choice": "sglang",
package/dist/runs.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Runs: one Gateway run per spec, streamed through typed events, ending in a classified RunEnd (5.8, O8).
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
+ import { authStatus } from "./auth-status.js";
4
5
  import { classify } from "./classify.js";
5
6
  // The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
6
7
  const WAIT_MS = 3_600_000;
@@ -125,7 +126,7 @@ export function createRuns(ctx) {
125
126
  // The picked account must be the one called: check it is signed in for this member before the run, since
126
127
  // the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
127
128
  // the pinned engine (never another provider or model), so this check plus the override is the guarantee.
128
- const status = (refresh) => ctx.request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) });
129
+ const status = (refresh) => authStatus(ctx.request, agentId, refresh, picked.provider === 'claude-cli');
129
130
  let auth = await status(false);
130
131
  if (auth.unavailable)
131
132
  auth = await status(true); // no prepared snapshot yet: build it once
package/dist/signin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GatewayTransport, Member, SignInView } from './types.ts';
1
+ import type { GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
2
2
  export type SignInCtx = {
3
3
  request: GatewayTransport['request'];
4
4
  ensure(member: Member): Promise<{
@@ -11,14 +11,11 @@ export type SignInCtx = {
11
11
  * `paste` hands a returned browser address to a waiting wizard text step. Every exit short of done cancels this
12
12
  * session and its callback listener only.
13
13
  */
14
- export declare function signIn(ctx: SignInCtx, member: Member, o: {
15
- authChoice: string;
16
- via?: 'browser' | 'code';
17
- }, on: (v: SignInView) => void): {
14
+ export declare function signIn(ctx: SignInCtx, member: Member, o: SignInOptions, on: (v: SignInView) => void): {
18
15
  paste(text: string): void;
19
16
  cancel(): void;
20
17
  done: Promise<SignInView>;
21
18
  };
22
- /** The providers this member's engine reports as signed in, from `models.authStatus` (a string or `{ provider }`). */
19
+ /** Usable provider profiles plus native Claude Code readiness reported by the engine. */
23
20
  export declare function providers(ctx: SignInCtx, member: Member, refresh?: boolean): Promise<string[]>;
24
21
  export declare function signOut(ctx: SignInCtx, member: Member, provider: string): Promise<void>;
package/dist/signin.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // Sign-in: the kit drives OpenClaw's own setup wizard loop and holds the ChatGPT callback port during a browser
2
- // sign-in; credentials never pass through kit or app (D11, 5.7).
2
+ // sign-in; OAuth credentials stay in the engine/CLI. Explicit API keys go only to engine activation (D11).
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { createServer } from 'node:http';
5
+ import { authStatus } from "./auth-status.js";
6
+ import { signedInProviders } from "./runs.js";
5
7
  import { words } from "./words.js";
6
8
  // The pin's wizard contract (2026.8.1): steps arrive only from `wizard.next` (wizard.status answers {status, error}
7
9
  // and never carries a step), and one session at a time holds the gateway's single setup admission.
@@ -31,6 +33,18 @@ export function signIn(ctx, member, o, on) {
31
33
  let settleStart;
32
34
  const startingSettled = new Promise((resolve) => { settleStart = resolve; });
33
35
  let over = false;
36
+ let approval = false;
37
+ let sawDeviceCode = false;
38
+ let codeExpired = false;
39
+ let codeTimer;
40
+ const stopApproval = () => {
41
+ approval = false;
42
+ if (codeTimer)
43
+ clearTimeout(codeTimer);
44
+ codeTimer = undefined;
45
+ };
46
+ const cancelled = () => ({ state: 'failed', via, why: 'declined', error: words('signin.cancelled') });
47
+ const expired = () => ({ state: 'failed', via, why: 'expired', error: words('signin.expired') });
34
48
  let settle;
35
49
  let pasteIn;
36
50
  let returned;
@@ -46,7 +60,11 @@ export function signIn(ctx, member, o, on) {
46
60
  if (over)
47
61
  return;
48
62
  over = true;
63
+ stopApproval();
64
+ o.signal?.removeEventListener('abort', cancel);
49
65
  owner.abort();
66
+ returned = undefined;
67
+ pasteIn = undefined;
50
68
  await closeCallback();
51
69
  on(view);
52
70
  settle(view);
@@ -63,6 +81,8 @@ export function signIn(ctx, member, o, on) {
63
81
  return releasing;
64
82
  };
65
83
  const paste = (text) => {
84
+ if (over)
85
+ return;
66
86
  if (pasteIn)
67
87
  pasteIn(text);
68
88
  else
@@ -71,9 +91,14 @@ export function signIn(ctx, member, o, on) {
71
91
  const cancel = () => {
72
92
  // Stop the drive now; the start itself is never aborted, and release waits for it to settle before cancelling,
73
93
  // so the gateway's one setup admission is freed by a cancel that the engine can actually find.
94
+ if (signal.aborted)
95
+ return;
74
96
  owner.abort();
75
- void (async () => { await release(); await finish({ state: 'failed', via, why: 'declined' }); })();
97
+ void (async () => { await release(); await finish(cancelled()); })();
76
98
  };
99
+ o.signal?.addEventListener('abort', cancel, { once: true });
100
+ if (o.signal?.aborted)
101
+ cancel();
77
102
  /** Hold 127.0.0.1:<callbackPort> for the sign-in's life; a taken port is why the sign-in cannot start. */
78
103
  const holdCallback = () => {
79
104
  let took = false;
@@ -111,8 +136,42 @@ export function signIn(ctx, member, o, on) {
111
136
  return { state: 'failed', via, why: 'busy' };
112
137
  }
113
138
  const { agentId } = await ctx.ensure(member);
114
- // The session id is the client's to choose (5.7), and the start request carries no abort signal: the engine
115
- // registers the session as it settles, so a cancel waits for it (release) rather than racing it.
139
+ // Native login is completed by the person in unmodified Claude Code. The pin does not expose this
140
+ // choice via auth.start; activation verifies the CLI route and persists it only after a live test succeeds.
141
+ if (o.authChoice === 'anthropic-cli' || o.authChoice === 'apiKey') {
142
+ settleStart();
143
+ let apiKey;
144
+ if (o.authChoice === 'anthropic-cli') {
145
+ const detected = await ctx.request('openclaw.setup.detect', { agentId }, { timeoutMs: STATUS_MS });
146
+ if (!detected.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true))
147
+ return { state: 'failed', via, why: 'failed', error: 'Sign in with Claude Code on this machine in the kit’s isolated HOME, then try again. The login stays in Claude Code.' };
148
+ }
149
+ else {
150
+ say({ prompt: 'API key (billed per use)' });
151
+ apiKey = returned ?? await new Promise((resolve) => {
152
+ const stop = (value) => { clearTimeout(timer); signal.removeEventListener('abort', abort); pasteIn = undefined; resolve(value); };
153
+ const abort = () => stop();
154
+ const timer = setTimeout(abort, PASTE_MS);
155
+ pasteIn = stop;
156
+ signal.addEventListener('abort', abort, { once: true });
157
+ if (signal.aborted)
158
+ abort();
159
+ });
160
+ returned = undefined;
161
+ if (!apiKey)
162
+ return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
163
+ }
164
+ if (signal.aborted)
165
+ return { state: 'failed', via, why: 'declined' };
166
+ const result = await ctx.request('openclaw.setup.activate', {
167
+ agentId, kind: apiKey === undefined ? 'claude-cli' : 'api-key',
168
+ ...(apiKey === undefined ? {} : { authChoice: 'apiKey', apiKey }),
169
+ }, { timeoutMs: PULL_MS, signal });
170
+ // API secrets must never be echoed in a view, even if the gateway returns an error containing one.
171
+ return result.ok ? { state: 'done', via } : { state: 'failed', via, why: 'failed',
172
+ error: apiKey === undefined ? cut(result.error ?? 'Claude Code activation failed') : 'API key activation failed' };
173
+ }
174
+ // The client chooses the wizard id; cancellation waits for the engine to register it.
116
175
  sessionId = `byokit-${randomUUID()}`;
117
176
  let started;
118
177
  try {
@@ -125,8 +184,9 @@ export function signIn(ctx, member, o, on) {
125
184
  if (started.done)
126
185
  return { state: 'done', via };
127
186
  // wizard.next, never wizard.status: the status method answers {status, error} and carries no step.
128
- const pull = () => ctx.request('wizard.next', { sessionId }, pullOptions(signal));
129
- const answer = (step, value) => ctx.request('wizard.next', { sessionId, answer: { stepId: step.id, ...(value === undefined ? {} : { value }) } }, pullOptions(signal));
187
+ const options = () => approval ? { timeoutMs: null, signal } : pullOptions(signal);
188
+ const pull = () => ctx.request('wizard.next', { sessionId }, options());
189
+ const answer = (step, value) => ctx.request('wizard.next', { sessionId, answer: { stepId: step.id, ...(value === undefined ? {} : { value }) } }, options());
130
190
  const waitForPaste = () => new Promise((resolve) => {
131
191
  if (returned !== undefined) {
132
192
  const text = returned;
@@ -166,6 +226,16 @@ export function signIn(ctx, member, o, on) {
166
226
  const current = step;
167
227
  step = undefined;
168
228
  if (current.deviceCode) {
229
+ stopApproval();
230
+ approval = true;
231
+ sawDeviceCode = true;
232
+ // The pin converts its provider duration to minutes. Prefer exact seconds if the engine supplies them.
233
+ const duration = current.deviceCode.expires_in !== undefined
234
+ ? Number(current.deviceCode.expires_in) * 1_000
235
+ : Number(current.deviceCode.expiresInMinutes) * 60_000;
236
+ if (Number.isFinite(duration) && duration >= 0) {
237
+ codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
238
+ }
169
239
  // The code is on the card; acknowledging it lets the engine poll the provider itself.
170
240
  say({ ...(current.deviceCode.code ? { code: current.deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}) });
171
241
  const next = await answer(current);
@@ -176,10 +246,12 @@ export function signIn(ctx, member, o, on) {
176
246
  step = next.step;
177
247
  continue;
178
248
  }
249
+ if (current.type !== 'progress')
250
+ stopApproval();
179
251
  if (current.type === 'text' && !current.sensitive) {
180
252
  const value = await waitForPaste();
181
253
  if (value === undefined)
182
- return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
254
+ return signal.aborted ? cancelled() : expired();
183
255
  const next = await answer(current, value);
184
256
  if (next.done) {
185
257
  terminal = next;
@@ -212,10 +284,12 @@ export function signIn(ctx, member, o, on) {
212
284
  step = next.step;
213
285
  }
214
286
  if (signal.aborted)
215
- return { state: 'failed', via, why: 'declined' };
287
+ return codeExpired ? expired() : cancelled();
216
288
  if (!terminal)
217
289
  return { state: 'failed', via, why: 'expired' };
218
290
  const error = String(terminal.error ?? (terminal.status === 'error' ? 'Sign-in failed' : ''));
291
+ if (sawDeviceCode && /expired_token|code.*expired|device.*(?:expired|timed out)/i.test(error))
292
+ return expired();
219
293
  return error ? { state: 'failed', via, why: 'failed', error: cut(error) } : { state: 'done', via };
220
294
  };
221
295
  void (async () => {
@@ -225,11 +299,13 @@ export function signIn(ctx, member, o, on) {
225
299
  }
226
300
  catch (error) {
227
301
  if (signal.aborted)
228
- view = { state: 'failed', via, why: 'declined' };
302
+ view = codeExpired ? expired() : cancelled();
303
+ else if (sawDeviceCode && /expired_token|code.*expired|device.*(?:expired|timed out)/i.test(cut(error)))
304
+ view = expired();
229
305
  else if (BUSY.test(cut(error)))
230
306
  view = { state: 'failed', via, why: 'busy' };
231
307
  else
232
- view = { state: 'failed', via, why: 'failed', error: cut(error) };
308
+ view = { state: 'failed', via, why: 'failed', error: o.authChoice === 'apiKey' ? 'API key activation failed' : cut(error) };
233
309
  }
234
310
  settleStart();
235
311
  if (view.state !== 'done')
@@ -238,12 +314,10 @@ export function signIn(ctx, member, o, on) {
238
314
  })();
239
315
  return { paste, cancel, done };
240
316
  }
241
- /** The providers this member's engine reports as signed in, from `models.authStatus` (a string or `{ provider }`). */
317
+ /** Usable provider profiles plus native Claude Code readiness reported by the engine. */
242
318
  export async function providers(ctx, member, refresh) {
243
319
  const { agentId } = await ctx.ensure(member);
244
- const status = await ctx.request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: STATUS_MS });
245
- const names = (status?.providers ?? []).map((entry) => (typeof entry === 'string' ? entry : entry?.provider));
246
- return [...new Set(names.filter((name) => typeof name === 'string' && name !== ''))];
320
+ return signedInProviders(await authStatus(ctx.request, agentId, refresh, true)) ?? [];
247
321
  }
248
322
  export async function signOut(ctx, member, provider) {
249
323
  const { agentId } = await ctx.ensure(member);
package/dist/types.d.ts CHANGED
@@ -83,11 +83,17 @@ export type RunEnd = {
83
83
  until?: number;
84
84
  message: string;
85
85
  };
86
+ export type SignInOptions = {
87
+ authChoice: string;
88
+ via?: 'browser' | 'code';
89
+ signal?: AbortSignal;
90
+ };
86
91
  export type SignInView = {
87
92
  state: 'waiting' | 'done' | 'failed';
88
93
  via: 'browser' | 'code';
89
94
  url?: string;
90
95
  code?: string;
96
+ prompt?: string;
91
97
  error?: string;
92
98
  why?: 'busy' | 'declined' | 'expired' | 'failed';
93
99
  };
@@ -109,7 +115,7 @@ export type Decision = {
109
115
  };
110
116
  export type KitState = {
111
117
  phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
112
- why?: 'install' | 'handshake' | 'exited' | 'port' | 'version';
118
+ why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running';
113
119
  retryAt?: number;
114
120
  };
115
121
  export type Hello = {
@@ -125,6 +131,9 @@ export type Route = {
125
131
  provider: string;
126
132
  plugin: string;
127
133
  billing: 'subscription' | 'api' | 'local';
134
+ auth?: 'cli' | 'api_key' | 'token';
135
+ terms?: 'allowed' | 'grey';
136
+ termsUrl?: string;
128
137
  via: 'browser' | 'code';
129
138
  prerequisite: string | null;
130
139
  offer: boolean;
@@ -134,7 +143,7 @@ export type Route = {
134
143
  export interface GatewayTransport {
135
144
  start(): Promise<Hello>;
136
145
  request(method: string, params?: unknown, o?: {
137
- timeoutMs?: number;
146
+ timeoutMs?: number | null;
138
147
  signal?: AbortSignal;
139
148
  expectFinal?: boolean;
140
149
  onAccepted?: (payload: unknown) => void;
@@ -147,7 +156,7 @@ export interface GatewayTransport {
147
156
  stop(): Promise<void>;
148
157
  }
149
158
  export type CallOptions = {
150
- timeoutMs?: number;
159
+ timeoutMs?: number | null;
151
160
  signal?: AbortSignal;
152
161
  };
153
162
  export type { GatewayMethod, GatewayParams, GatewayResult } from './generated/methods.ts';
package/dist/words.js CHANGED
@@ -19,6 +19,8 @@ const PHASE_WORDS = {
19
19
  };
20
20
  export function stateWords(s) {
21
21
  // 'stopped' is never on screen (4.3 covers what a person can see), so 5.14 has no row for it.
22
+ if (s.phase === 'failed' && s.why === 'engine-already-running')
23
+ return words('engine.alreadyRunning');
22
24
  return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
23
25
  }
24
26
  export function toAccountView(view, ready) {
package/dist/words.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "engine.locked": "Your saved sign-in is locked. Unlock your password storage, then try again.",
6
6
  "engine.ready": "Ready.",
7
7
  "engine.restarting": "Something stopped. Starting it again by itself.",
8
+ "engine.alreadyRunning": "Your saved sign-in is in use. Try again after the other session stops.",
8
9
  "engine.failed": "This computer couldn't start the helper. Restart the app to try again.",
9
10
  "engine.needsUpdate": "This app needs an update to keep working.",
10
11
  "member.signedOut": "Sign in with {name} to start.",
@@ -13,6 +14,7 @@
13
14
  "member.network": "Can't reach {name} right now. This keeps trying by itself.",
14
15
  "signin.returned": "Thanks. Finishing the sign-in — you can go back to the app now.",
15
16
  "signin.busy": "Another sign-in is already in progress. Finish or cancel it, then try again.",
17
+ "signin.cancelled": "Sign-in cancelled. You can start again whenever you are ready.",
16
18
  "signin.expired": "The sign-in took too long. Start it again.",
17
19
  "approval.ask": "{helper} wants to {summary}. Allow it?",
18
20
  "approval.expired": "Nobody answered in time, so this wasn't allowed.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",