@compilr-dev/sdk 0.10.41 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * runDeviceFlow — browser-based (OAuth device flow) login state machine.
3
+ *
4
+ * Ported from the CLI's AuthManager.loginWithDeviceFlow so CLI and Desktop share
5
+ * one implementation. Desktop previously drove the poll loop from the renderer
6
+ * one fetch at a time and therefore lacked the `slow_down` backoff this owns.
7
+ *
8
+ * On success the tokens are persisted to `~/.compilr-dev/auth.json`. Fetching the
9
+ * user profile (account type, etc.) is left to the host — call `getProfile`
10
+ * afterward if needed.
11
+ */
12
+ import { requestDeviceCode, pollDeviceToken, getAuthorizationUrl } from './auth-api.js';
13
+ import { saveAuthData } from './auth-store.js';
14
+ const NETWORK_RETRY_MS = 2000;
15
+ const MAX_POLL_INTERVAL_MS = 30_000;
16
+ const SLOW_DOWN_STEP_MS = 5000;
17
+ /**
18
+ * Next poll interval after a `slow_down` response: +5s, capped at 30s.
19
+ * Exported for unit testing the backoff that Desktop was missing.
20
+ */
21
+ export function nextPollInterval(currentMs) {
22
+ return Math.min(currentMs + SLOW_DOWN_STEP_MS, MAX_POLL_INTERVAL_MS);
23
+ }
24
+ /** Wait `ms`, resolving early (true) if the signal aborts. */
25
+ function waitOrAbort(ms, signal) {
26
+ return new Promise((resolve) => {
27
+ const timeout = setTimeout(() => {
28
+ signal?.removeEventListener('abort', onAbort);
29
+ resolve(false);
30
+ }, ms);
31
+ const onAbort = () => {
32
+ clearTimeout(timeout);
33
+ resolve(true);
34
+ };
35
+ signal?.addEventListener('abort', onAbort, { once: true });
36
+ });
37
+ }
38
+ /**
39
+ * Run the full device-flow login: request a code, surface it via `onCodeReady`,
40
+ * then poll until authorized, denied, expired, or aborted. Persists tokens on
41
+ * success.
42
+ */
43
+ export async function runDeviceFlow(callbacks, signal, opts) {
44
+ // Step 1: request a device code.
45
+ const codeResult = await requestDeviceCode(opts?.endpoints);
46
+ if (!codeResult.success || !codeResult.data) {
47
+ return { success: false, error: codeResult.error ?? 'Failed to get device code' };
48
+ }
49
+ const { device_code, user_code, expires_in, interval } = codeResult.data;
50
+ // Step 2: surface the code + URL (the host shows it and/or opens the browser).
51
+ callbacks.onCodeReady(user_code, getAuthorizationUrl(user_code, opts?.endpoints));
52
+ // Step 3: poll until a terminal outcome.
53
+ const expiresAtMs = Date.now() + expires_in * 1000;
54
+ let pollIntervalMs = interval * 1000;
55
+ while (Date.now() < expiresAtMs) {
56
+ if (signal?.aborted)
57
+ return { success: false, error: 'Login cancelled' };
58
+ const aborted = await waitOrAbort(pollIntervalMs, signal);
59
+ if (aborted || signal?.aborted)
60
+ return { success: false, error: 'Login cancelled' };
61
+ callbacks.onStatusUpdate?.('polling');
62
+ const tokenResult = await pollDeviceToken(device_code, opts?.endpoints);
63
+ if (tokenResult.success && tokenResult.data) {
64
+ callbacks.onStatusUpdate?.('authorized');
65
+ const { access_token, refresh_token, api_token, expires_in: tokenExpiresIn, user, } = tokenResult.data;
66
+ const authData = {
67
+ version: 1,
68
+ user: {
69
+ id: user.id,
70
+ email: user.email,
71
+ // Device flow doesn't return account creation time.
72
+ createdAt: new Date().toISOString(),
73
+ },
74
+ session: {
75
+ accessToken: access_token,
76
+ refreshToken: refresh_token,
77
+ expiresAt: new Date(Date.now() + tokenExpiresIn * 1000).toISOString(),
78
+ apiToken: api_token,
79
+ },
80
+ };
81
+ saveAuthData(authData, opts?.dir);
82
+ return { success: true, user: { id: user.id, email: user.email } };
83
+ }
84
+ if (tokenResult.error) {
85
+ switch (tokenResult.error.error) {
86
+ case 'authorization_pending':
87
+ continue;
88
+ case 'slow_down':
89
+ pollIntervalMs = nextPollInterval(pollIntervalMs);
90
+ continue;
91
+ case 'expired_token':
92
+ callbacks.onStatusUpdate?.('expired');
93
+ return { success: false, error: 'Authorization expired. Please try again.' };
94
+ case 'access_denied':
95
+ callbacks.onStatusUpdate?.('denied');
96
+ return { success: false, error: 'Authorization denied.' };
97
+ default:
98
+ return { success: false, error: tokenResult.error.error_description };
99
+ }
100
+ }
101
+ // Transient network error — wait a little longer and retry.
102
+ if (tokenResult.networkError) {
103
+ const cancelled = await waitOrAbort(NETWORK_RETRY_MS, signal);
104
+ if (cancelled)
105
+ return { success: false, error: 'Login cancelled' };
106
+ continue;
107
+ }
108
+ }
109
+ callbacks.onStatusUpdate?.('expired');
110
+ return { success: false, error: 'Authorization timed out. Please try again.' };
111
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Host integration layer — shared schema and helpers for the things CLI and
3
+ * Desktop both persist under `~/.compilr-dev/` (settings today; auth/token and
4
+ * device flow to follow in Phase 2). Pure data + transforms; no fs/electron.
5
+ */
6
+ export { type HostSettings, type SettingsProviderType, type TextSize, type NotificationMode, type Verbosity, type CompactMode, type ProjectSessionMode, type MascotSetting, type StartupMode, type ProjectStartupMode, type StartupBehavior, type DefaultLeftPanel, type TipsProficiency, SHARED_DEFAULTS, migrateRawSettings, } from './settings-schema.js';
7
+ export type { AccountType, AuthUser, AuthSession, AuthData, RefreshTokenResponse, ProfileData, HeartbeatData, DeviceCodeResponse, DeviceTokenResponse, DeviceTokenError, AuthEndpoints, } from './auth-types.js';
8
+ export { refreshToken, sendHeartbeat, getProfile, updateProfile, requestDeviceCode, pollDeviceToken, getAuthorizationUrl, } from './auth-api.js';
9
+ export { loadAuthData, saveAuthData, clearAuthData, updateSession, hasAuthData, checkAuthFilePermissions, readAuthFile, writeAuthFile, } from './auth-store.js';
10
+ export { ensureFreshToken } from './token.js';
11
+ export type { EnsureFreshTokenOptions, FreshToken } from './token.js';
12
+ export { runDeviceFlow, nextPollInterval } from './device-flow.js';
13
+ export type { DeviceFlowStatus, DeviceFlowCallbacks, LoginResult, RunDeviceFlowOptions, } from './device-flow.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Host integration layer — shared schema and helpers for the things CLI and
3
+ * Desktop both persist under `~/.compilr-dev/` (settings today; auth/token and
4
+ * device flow to follow in Phase 2). Pure data + transforms; no fs/electron.
5
+ */
6
+ export { SHARED_DEFAULTS, migrateRawSettings, } from './settings-schema.js';
7
+ export { refreshToken, sendHeartbeat, getProfile, updateProfile, requestDeviceCode, pollDeviceToken, getAuthorizationUrl, } from './auth-api.js';
8
+ export { loadAuthData, saveAuthData, clearAuthData, updateSession, hasAuthData, checkAuthFilePermissions, readAuthFile, writeAuthFile, } from './auth-store.js';
9
+ export { ensureFreshToken } from './token.js';
10
+ export { runDeviceFlow, nextPollInterval } from './device-flow.js';
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Shared host settings schema, defaults, and migrations.
3
+ *
4
+ * CLI and Desktop both persist a `settings.json` under `~/.compilr-dev/`. They
5
+ * historically described it with two independent TypeScript interfaces and only
6
+ * the CLI ran value migrations — so a value the CLI would migrate was consumed
7
+ * raw by Desktop, and fields one app didn't know about survived only by luck.
8
+ *
9
+ * This module is the single source of truth for:
10
+ * - the union shape of the file (`HostSettings`),
11
+ * - the value-level migrations (`migrateRawSettings`), and
12
+ * - the defaults whose semantics are identical across hosts (`SHARED_DEFAULTS`).
13
+ *
14
+ * File I/O, caching, per-host defaults, and the encryption master key stay in
15
+ * each host (see host-drift-remediation-spec.md §4.5). This module is pure:
16
+ * no `fs`, no `electron`, no process globals — it only transforms plain objects.
17
+ */
18
+ import { type PermissionRule, type PermissionMode } from '../permissions.js';
19
+ import type { ProviderType } from '../config.js';
20
+ /**
21
+ * Provider identifier as stored in settings. Superset of the agent-config
22
+ * `ProviderType`: it adds `'auto'`, meaning "resolve from the environment" via
23
+ * `detectProviderFromEnv()`. Hosts without auto-detection should treat `'auto'`
24
+ * as a request to pick a concrete provider, never as invalid.
25
+ */
26
+ export type SettingsProviderType = ProviderType | 'auto';
27
+ export type TextSize = 'small' | 'medium' | 'large';
28
+ export type NotificationMode = 'auto' | 'bell' | 'disabled';
29
+ export type Verbosity = 'normal' | 'focused' | 'verbose';
30
+ export type CompactMode = 'active' | 'all' | 'auto';
31
+ export type ProjectSessionMode = 'auto' | 'ask' | 'fresh';
32
+ export type MascotSetting = 'none' | 'random' | 'neutral' | 'thinking' | 'looking_left' | 'looking_right' | 'sleeping' | 'alert' | 'error' | 'success' | 'success_minimal' | 'searching' | 'skeptical';
33
+ /** CLI: how the CLI starts up (interactive menu vs straight into the REPL). */
34
+ export type StartupMode = 'menu' | 'repl';
35
+ /** CLI: whether to auto-load the last project on startup. */
36
+ export type ProjectStartupMode = 'last' | 'off';
37
+ /** Desktop: resume the last project or land on the dashboard. */
38
+ export type StartupBehavior = 'last-project' | 'dashboard';
39
+ /** Desktop: which side panel opens by default ('none' = collapsed). */
40
+ export type DefaultLeftPanel = 'none' | 'project' | 'conversations' | 'agents' | 'backlog' | 'docs';
41
+ /** Desktop: status-bar tips proficiency ('auto' derives from usage). */
42
+ export type TipsProficiency = 'auto' | 'beginner' | 'intermediate' | 'pro';
43
+ /**
44
+ * The full shape of `~/.compilr-dev/settings.json`.
45
+ *
46
+ * Every field is optional at the type level; required-ness is produced by
47
+ * merging defaults at load time (`{ ...SHARED_DEFAULTS, ...HOST_DEFAULTS,
48
+ * ...migrated }`). Sections are grouped as shared / CLI-only / Desktop-only,
49
+ * but the file is flat and either host may carry the other's keys (preserved
50
+ * on save — see `migrateRawSettings`).
51
+ */
52
+ export interface HostSettings {
53
+ theme?: string;
54
+ firstRunComplete?: boolean;
55
+ /** Opt-out telemetry (default on). */
56
+ telemetryEnabled?: boolean;
57
+ defaultProvider?: SettingsProviderType;
58
+ /** null = use the provider's default model. */
59
+ defaultModel?: string | null;
60
+ ollamaBaseUrl?: string;
61
+ checkUpdates?: boolean;
62
+ /**
63
+ * Startup default mode. Session-level changes do NOT persist here.
64
+ * Authoritative for Desktop. See §4.2 of the host-drift spec.
65
+ */
66
+ defaultPermissionMode?: PermissionMode;
67
+ /**
68
+ * Last active mode, restored at startup for continuity. Authoritative for
69
+ * the CLI. Distinct from `defaultPermissionMode` by design — both are kept.
70
+ */
71
+ permissionMode?: PermissionMode;
72
+ permissionRules?: PermissionRule[];
73
+ autoCompact?: boolean;
74
+ showTips?: boolean;
75
+ reviseCode?: boolean;
76
+ /** @deprecated superseded by `verbosity`; migrated forward. */
77
+ verbose?: boolean;
78
+ verbosity?: Verbosity;
79
+ progressBar?: boolean;
80
+ lastUpdateCheck?: number | null;
81
+ notifications?: NotificationMode;
82
+ startupMode?: StartupMode;
83
+ projectStartup?: ProjectStartupMode;
84
+ lastProjectId?: number | null;
85
+ mascot?: MascotSetting;
86
+ projectSessionMode?: ProjectSessionMode;
87
+ sessionRetentionDays?: number;
88
+ compactMode?: CompactMode;
89
+ /** Tool-result auto-delegation (summarize large results). */
90
+ delegation?: boolean;
91
+ vsDiffExtension?: boolean;
92
+ lastSeenVersion?: string;
93
+ modelTiers?: Partial<Record<SettingsProviderType, Partial<Record<'fast' | 'balanced' | 'powerful', string>>>>;
94
+ roleTierDefaults?: Record<string, 'fast' | 'balanced' | 'powerful'>;
95
+ textSize?: TextSize;
96
+ workspacePath?: string;
97
+ projectsPath?: string;
98
+ dataPath?: string;
99
+ deleteProtection?: boolean;
100
+ requireProjectMatch?: boolean;
101
+ /** Extended context window (1M for Claude); long-context pricing above 200K. */
102
+ extendedContext?: boolean;
103
+ startupBehavior?: StartupBehavior;
104
+ tourCompleted?: boolean;
105
+ defaultLeftPanel?: DefaultLeftPanel;
106
+ activityBarTooltipsEnabled?: boolean;
107
+ dudeEnabled?: boolean;
108
+ dudeAutoOpenOnFirstProject?: boolean;
109
+ dudeWelcomeShown?: boolean;
110
+ dudeModel?: string;
111
+ tipsEnabled?: boolean;
112
+ tipsProficiency?: TipsProficiency;
113
+ /**
114
+ * Encryption master key for the API-key keystore. Managed by each host's
115
+ * key storage; declared here so migrations preserve it. Never read by this
116
+ * module.
117
+ */
118
+ mk?: string;
119
+ }
120
+ /**
121
+ * Defaults for fields that mean the same thing in CLI and Desktop. Per-host
122
+ * defaults (e.g. CLI `defaultProvider: 'auto'` vs Desktop `'claude'`, plus each
123
+ * host's exclusive fields) are layered on top by the host:
124
+ *
125
+ * const merged = { ...SHARED_DEFAULTS, ...HOST_DEFAULTS, ...migrated };
126
+ */
127
+ export declare const SHARED_DEFAULTS: Partial<HostSettings>;
128
+ /**
129
+ * Apply all value-level migrations to a raw settings object read from disk.
130
+ *
131
+ * Pure and idempotent: `migrateRawSettings(migrateRawSettings(x).settings)`
132
+ * yields the same settings with `changed: false`. **Unknown keys are preserved**
133
+ * (the input is spread first) — this is the contract that lets an old and a new
134
+ * app version share the same file without dropping each other's fields.
135
+ *
136
+ * Hosts call this on load and persist the result when `changed` is true.
137
+ *
138
+ * Migrations (seeded from the CLI's historical set + one cross-host addition):
139
+ * 1. `verbose: true` and no `verbosity` → `verbosity: 'verbose'`
140
+ * 2. `projectStartup: 'cwd'` → `'off'`
141
+ * 3. `permissionMode` / `defaultPermissionMode` legacy values normalized
142
+ * 4. `defaultPermissionMode` absent but `permissionMode` present → seed it
143
+ * (gives Desktop a sane startup default on CLI-authored files)
144
+ */
145
+ export declare function migrateRawSettings(raw: Record<string, unknown>): {
146
+ settings: Record<string, unknown>;
147
+ changed: boolean;
148
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Shared host settings schema, defaults, and migrations.
3
+ *
4
+ * CLI and Desktop both persist a `settings.json` under `~/.compilr-dev/`. They
5
+ * historically described it with two independent TypeScript interfaces and only
6
+ * the CLI ran value migrations — so a value the CLI would migrate was consumed
7
+ * raw by Desktop, and fields one app didn't know about survived only by luck.
8
+ *
9
+ * This module is the single source of truth for:
10
+ * - the union shape of the file (`HostSettings`),
11
+ * - the value-level migrations (`migrateRawSettings`), and
12
+ * - the defaults whose semantics are identical across hosts (`SHARED_DEFAULTS`).
13
+ *
14
+ * File I/O, caching, per-host defaults, and the encryption master key stay in
15
+ * each host (see host-drift-remediation-spec.md §4.5). This module is pure:
16
+ * no `fs`, no `electron`, no process globals — it only transforms plain objects.
17
+ */
18
+ import { DEFAULT_PERMISSION_RULES, } from '../permissions.js';
19
+ // =============================================================================
20
+ // Shared defaults (identical semantics across hosts only)
21
+ // =============================================================================
22
+ /**
23
+ * Defaults for fields that mean the same thing in CLI and Desktop. Per-host
24
+ * defaults (e.g. CLI `defaultProvider: 'auto'` vs Desktop `'claude'`, plus each
25
+ * host's exclusive fields) are layered on top by the host:
26
+ *
27
+ * const merged = { ...SHARED_DEFAULTS, ...HOST_DEFAULTS, ...migrated };
28
+ */
29
+ export const SHARED_DEFAULTS = {
30
+ theme: 'compilr-dark',
31
+ firstRunComplete: false,
32
+ telemetryEnabled: true,
33
+ checkUpdates: true,
34
+ defaultModel: null,
35
+ defaultPermissionMode: 'normal',
36
+ permissionRules: [...DEFAULT_PERMISSION_RULES],
37
+ };
38
+ // =============================================================================
39
+ // Migrations
40
+ // =============================================================================
41
+ /** Normalize a legacy permission-mode value to the current vocabulary. */
42
+ function migratePermissionModeValue(value) {
43
+ switch (value) {
44
+ case 'default':
45
+ case 'accept-edits':
46
+ return 'normal';
47
+ case 'dont-ask':
48
+ return 'auto-accept';
49
+ case 'normal':
50
+ case 'plan':
51
+ case 'auto-accept':
52
+ return value;
53
+ default:
54
+ return undefined;
55
+ }
56
+ }
57
+ /**
58
+ * Apply all value-level migrations to a raw settings object read from disk.
59
+ *
60
+ * Pure and idempotent: `migrateRawSettings(migrateRawSettings(x).settings)`
61
+ * yields the same settings with `changed: false`. **Unknown keys are preserved**
62
+ * (the input is spread first) — this is the contract that lets an old and a new
63
+ * app version share the same file without dropping each other's fields.
64
+ *
65
+ * Hosts call this on load and persist the result when `changed` is true.
66
+ *
67
+ * Migrations (seeded from the CLI's historical set + one cross-host addition):
68
+ * 1. `verbose: true` and no `verbosity` → `verbosity: 'verbose'`
69
+ * 2. `projectStartup: 'cwd'` → `'off'`
70
+ * 3. `permissionMode` / `defaultPermissionMode` legacy values normalized
71
+ * 4. `defaultPermissionMode` absent but `permissionMode` present → seed it
72
+ * (gives Desktop a sane startup default on CLI-authored files)
73
+ */
74
+ export function migrateRawSettings(raw) {
75
+ const out = { ...raw };
76
+ let changed = false;
77
+ // 1. verbose → verbosity
78
+ if (!('verbosity' in out) && out['verbose'] === true) {
79
+ out['verbosity'] = 'verbose';
80
+ changed = true;
81
+ }
82
+ // 2. projectStartup 'cwd' → 'off'
83
+ if (out['projectStartup'] === 'cwd') {
84
+ out['projectStartup'] = 'off';
85
+ changed = true;
86
+ }
87
+ // 3. Normalize legacy permission-mode values on both fields.
88
+ for (const key of ['permissionMode', 'defaultPermissionMode']) {
89
+ if (key in out) {
90
+ const normalized = migratePermissionModeValue(out[key]);
91
+ if (normalized !== undefined && normalized !== out[key]) {
92
+ out[key] = normalized;
93
+ changed = true;
94
+ }
95
+ }
96
+ }
97
+ // 4. Seed defaultPermissionMode from permissionMode when absent.
98
+ if ((out['defaultPermissionMode'] === undefined || !('defaultPermissionMode' in out)) &&
99
+ typeof out['permissionMode'] === 'string') {
100
+ const seeded = migratePermissionModeValue(out['permissionMode']);
101
+ if (seeded !== undefined) {
102
+ out['defaultPermissionMode'] = seeded;
103
+ changed = true;
104
+ }
105
+ }
106
+ return { settings: out, changed };
107
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ensureFreshToken — return a usable bearer token, refreshing the JWT if needed.
3
+ *
4
+ * This is the fix for the Desktop JWT-lockout bug: Desktop read `auth.json` but
5
+ * had no refresh path, so once the JWT expired (and no long-lived `apiToken` was
6
+ * present) every backend call failed until the user logged in again. Both hosts
7
+ * now route token access through here.
8
+ *
9
+ * State machine (ported from the CLI's AuthManager.isAuthenticated/getAccessToken):
10
+ * - `apiToken` present → return it (server validates per-request; no expiry)
11
+ * - JWT with > threshold remaining → return the access token as-is
12
+ * - otherwise → refresh via the backend, persist the new session, return it
13
+ * - refresh failure → return null, but DO NOT delete auth.json (the refresh
14
+ * token may still be valid after a transient/network error; only explicit
15
+ * logout clears credentials)
16
+ */
17
+ import type { AuthEndpoints } from './auth-types.js';
18
+ export interface EnsureFreshTokenOptions {
19
+ /** Refresh the JWT when it has less than this many ms left. Default 5 min. */
20
+ thresholdMs?: number;
21
+ /** Override the `.compilr-dev` directory (tests / non-standard homes). */
22
+ dir?: string;
23
+ /** Override backend endpoints (tests / branch previews). */
24
+ endpoints?: AuthEndpoints;
25
+ }
26
+ export interface FreshToken {
27
+ token: string;
28
+ source: 'api-token' | 'jwt' | 'refreshed';
29
+ }
30
+ /**
31
+ * Resolve the best available token, refreshing if the JWT is near expiry.
32
+ * Returns null when there are no credentials or a needed refresh failed.
33
+ */
34
+ export declare function ensureFreshToken(opts?: EnsureFreshTokenOptions): Promise<FreshToken | null>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * ensureFreshToken — return a usable bearer token, refreshing the JWT if needed.
3
+ *
4
+ * This is the fix for the Desktop JWT-lockout bug: Desktop read `auth.json` but
5
+ * had no refresh path, so once the JWT expired (and no long-lived `apiToken` was
6
+ * present) every backend call failed until the user logged in again. Both hosts
7
+ * now route token access through here.
8
+ *
9
+ * State machine (ported from the CLI's AuthManager.isAuthenticated/getAccessToken):
10
+ * - `apiToken` present → return it (server validates per-request; no expiry)
11
+ * - JWT with > threshold remaining → return the access token as-is
12
+ * - otherwise → refresh via the backend, persist the new session, return it
13
+ * - refresh failure → return null, but DO NOT delete auth.json (the refresh
14
+ * token may still be valid after a transient/network error; only explicit
15
+ * logout clears credentials)
16
+ */
17
+ import { refreshToken as apiRefreshToken } from './auth-api.js';
18
+ import { loadAuthData, updateSession } from './auth-store.js';
19
+ const DEFAULT_THRESHOLD_MS = 5 * 60 * 1000; // refresh when < 5 min remaining
20
+ const DEFAULT_EXPIRES_IN_S = 3600; // assume 1h if the server omits expires_in
21
+ /**
22
+ * Resolve the best available token, refreshing if the JWT is near expiry.
23
+ * Returns null when there are no credentials or a needed refresh failed.
24
+ */
25
+ export async function ensureFreshToken(opts) {
26
+ const auth = loadAuthData(opts?.dir);
27
+ if (!auth)
28
+ return null;
29
+ // Long-lived API token never expires locally.
30
+ if (auth.session.apiToken) {
31
+ return { token: auth.session.apiToken, source: 'api-token' };
32
+ }
33
+ const thresholdMs = opts?.thresholdMs ?? DEFAULT_THRESHOLD_MS;
34
+ const expiresAt = new Date(auth.session.expiresAt).getTime();
35
+ const remaining = expiresAt - Date.now();
36
+ // JWT still comfortably valid.
37
+ if (Number.isFinite(expiresAt) && remaining >= thresholdMs) {
38
+ return { token: auth.session.accessToken, source: 'jwt' };
39
+ }
40
+ // Needs a refresh.
41
+ if (!auth.session.refreshToken)
42
+ return null;
43
+ const result = await apiRefreshToken(auth.session.refreshToken, opts?.endpoints);
44
+ if (!result.success || !result.access_token || !result.refresh_token) {
45
+ // Preserve auth.json on disk — the refresh token may still be valid.
46
+ return null;
47
+ }
48
+ const expiresIn = result.expires_in ?? DEFAULT_EXPIRES_IN_S;
49
+ const newSession = {
50
+ accessToken: result.access_token,
51
+ refreshToken: result.refresh_token,
52
+ expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
53
+ };
54
+ updateSession(newSession, opts?.dir);
55
+ return { token: result.access_token, source: 'refreshed' };
56
+ }
package/dist/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemReposi
58
58
  export type { SQLiteRepositories, CreateSQLiteRepositoriesOptions, ProjectDeleteHooks, ProjectRecord, WorkItemRecord, ProjectDocumentRecord, WorkItemCommentRecord, } from './platform/index.js';
59
59
  export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool, createInteractiveFlowTool, validateFlow, INTERACTIVE_FLOW_INPUT_SCHEMA, } from './tools/index.js';
60
60
  export type { AskUserQuestion, AskUserInput, AskUserResult, AskUserHandler, AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleHandler, Alternative, ProposeAlternativesInput, ProposeAlternativesResult, ProposeAlternativesHandler, Flow, FlowTone, FlowNode, QuestionNode, InfoNode, BranchNode, SummaryNode, QuestionInput, Choice, Proposal, NextRef, BranchRoute, BranchCondition, NodeId, IconName, Tint, RenderVariant, AnswerValue, AbortReason, InteractiveFlowInput, InteractiveFlowResult, InteractiveFlowHandler, FlowValidationResult, FlowValidationError, FlowErrorCode, } from './tools/index.js';
61
- export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS, DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, } from './entitlements/index.js';
61
+ export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS, DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, fetchEntitlements, } from './entitlements/index.js';
62
62
  export type { TierLimits, EntitlementResponse, LimitCheckResult, IEntitlementStore, EntitlementCacheConfig, } from './entitlements/index.js';
63
63
  export { detectProject, suggestProjectType, detectCommon } from './detection/index.js';
64
64
  export type { DetectProjectOptions, DetectionResult, ContentSummary } from './detection/index.js';
@@ -79,8 +79,12 @@ export type { ActionMeta, SkillMeta } from './project-types/index.js';
79
79
  export type { ProjectTypeConfig, ProjectPhase, SuggestedAgent, DocumentTemplate, } from './project-types/index.js';
80
80
  export { defineTool, createSuccessResult, createErrorResult, mergeHooks, createLoggingHooks, createClaudeProvider, createOpenAIProvider, createGeminiNativeProvider, createOllamaProvider, createTogetherProvider, createGroqProvider, createFireworksProvider, createPerplexityProvider, createOpenRouterProvider, createMockProvider, MockProvider, Agent, ContextManager, DEFAULT_CONTEXT_CONFIG, createTaskTool, createSuggestTool, defaultAgentTypes, TOOL_SETS, BUILTIN_GUARDRAILS, TOOL_NAMES, getDefaultShellManager, builtinSkills, AnchorManager, MCPManager, AgentError, ProviderError, ToolError, ToolTimeoutError, MaxIterationsError, AbortError, } from '@compilr-dev/agents';
81
81
  export type { Tool, HooksConfig, AgentEvent, Message, LLMProvider, AnchorInput, ToolExecutionResult, AgentRunResult, PermissionHandler, PermissionHandlerResponse, ToolPermission, AgentTypeConfig, GuardrailTriggeredHandler, BeforeLLMHookResult, BeforeToolHook, BeforeToolHookResult, AfterToolHook, AgentState, AgentConfig, SessionInfo, Anchor, AnchorScope, AnchorClearOptions, AnchorPriority, AnchorQueryOptions, FileAccessType, FileAccess, GuardrailResult, GuardrailContext, MCPClient, MCPToolDefinition, } from '@compilr-dev/agents';
82
- export { DEFAULT_PERMISSION_RULES, findMatchingRule, permissionModeLabel, permissionLevelLabel, } from './permissions.js';
82
+ export { DEFAULT_PERMISSION_RULES, WRITE_TOOLS, findMatchingRule, permissionModeLabel, permissionLevelLabel, } from './permissions.js';
83
83
  export type { PermissionRule, PermissionMode, PermissionLevel } from './permissions.js';
84
+ export { SHARED_DEFAULTS, migrateRawSettings } from './host/index.js';
85
+ export type { HostSettings, SettingsProviderType, TextSize, NotificationMode, Verbosity, CompactMode, ProjectSessionMode, MascotSetting, StartupMode, ProjectStartupMode, StartupBehavior, DefaultLeftPanel, TipsProficiency, } from './host/index.js';
86
+ export { refreshToken, sendHeartbeat, getProfile, updateProfile, requestDeviceCode, pollDeviceToken, getAuthorizationUrl, loadAuthData, saveAuthData, clearAuthData, updateSession, hasAuthData, checkAuthFilePermissions, readAuthFile, writeAuthFile, ensureFreshToken, runDeviceFlow, nextPollInterval, } from './host/index.js';
87
+ export type { AccountType, AuthUser, AuthSession, AuthData, RefreshTokenResponse, ProfileData, HeartbeatData, DeviceCodeResponse, DeviceTokenResponse, DeviceTokenError, AuthEndpoints, EnsureFreshTokenOptions, FreshToken, DeviceFlowStatus, DeviceFlowCallbacks, LoginResult, RunDeviceFlowOptions, } from './host/index.js';
84
88
  export { DEFAULT_DELEGATION_CONFIG } from './delegation.js';
85
89
  export { FileEpisodeStore, EpisodeRecorder, isSignificantWork, extractAffectedFiles, extractLinesChanged, queryWorkAtRisk, buildWorkSummaryContent, updateWorkSummaryAnchor, } from './episodes/index.js';
86
90
  export type { FileEpisodeStoreOptions, EpisodeRecorderConfig, WorkSummaryAnchorConfig, PendingToolSignal, EpisodeFile, } from './episodes/index.js';
package/dist/index.js CHANGED
@@ -135,7 +135,7 @@ export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTo
135
135
  // =============================================================================
136
136
  // Entitlements (server-driven tier management)
137
137
  // =============================================================================
138
- export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS, DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, } from './entitlements/index.js';
138
+ export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS, DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, fetchEntitlements, } from './entitlements/index.js';
139
139
  // =============================================================================
140
140
  // Project Detection (universal project content detection)
141
141
  // =============================================================================
@@ -196,7 +196,13 @@ AgentError, ProviderError, ToolError, ToolTimeoutError, MaxIterationsError, Abor
196
196
  // =============================================================================
197
197
  // Shared Permission Defaults & Utilities
198
198
  // =============================================================================
199
- export { DEFAULT_PERMISSION_RULES, findMatchingRule, permissionModeLabel, permissionLevelLabel, } from './permissions.js';
199
+ export { DEFAULT_PERMISSION_RULES, WRITE_TOOLS, findMatchingRule, permissionModeLabel, permissionLevelLabel, } from './permissions.js';
200
+ // =============================================================================
201
+ // Host Integration Layer (shared settings schema + migrations)
202
+ // =============================================================================
203
+ export { SHARED_DEFAULTS, migrateRawSettings } from './host/index.js';
204
+ // Auth: shared backend client, credential store, token refresh, device flow.
205
+ export { refreshToken, sendHeartbeat, getProfile, updateProfile, requestDeviceCode, pollDeviceToken, getAuthorizationUrl, loadAuthData, saveAuthData, clearAuthData, updateSession, hasAuthData, checkAuthFilePermissions, readAuthFile, writeAuthFile, ensureFreshToken, runDeviceFlow, nextPollInterval, } from './host/index.js';
200
206
  // =============================================================================
201
207
  // Shared Delegation Config Defaults
202
208
  // =============================================================================
@@ -36,6 +36,14 @@ export type PermissionMode = 'normal' | 'plan' | 'auto-accept';
36
36
  * - Runner tools (run_tests, run_lint) → ask once
37
37
  */
38
38
  export declare const DEFAULT_PERMISSION_RULES: PermissionRule[];
39
+ /**
40
+ * Tools that mutate the workspace (filesystem or shell). Desktop's workspace-trust
41
+ * "restricted mode" blocks exactly these in untrusted projects; previously it
42
+ * hand-maintained a parallel list that could silently drift from the SDK. This is
43
+ * the single source of truth — a subset of `PLAN_MODE_BLOCKED_TOOLS` (which also
44
+ * blocks git/runner tools), enforced by a test so the two can't diverge.
45
+ */
46
+ export declare const WRITE_TOOLS: ReadonlySet<string>;
39
47
  /**
40
48
  * Find the matching permission rule for a tool name.
41
49
  * Checks exact match first, then wildcard patterns (e.g., git_* matches git_commit).
@@ -34,6 +34,25 @@ export const DEFAULT_PERMISSION_RULES = [
34
34
  { toolName: 'grep', level: 'always', description: 'Search file contents', isDefault: true },
35
35
  ];
36
36
  // =============================================================================
37
+ // Write-tool set (workspace trust / restricted mode)
38
+ // =============================================================================
39
+ /**
40
+ * Tools that mutate the workspace (filesystem or shell). Desktop's workspace-trust
41
+ * "restricted mode" blocks exactly these in untrusted projects; previously it
42
+ * hand-maintained a parallel list that could silently drift from the SDK. This is
43
+ * the single source of truth — a subset of `PLAN_MODE_BLOCKED_TOOLS` (which also
44
+ * blocks git/runner tools), enforced by a test so the two can't diverge.
45
+ */
46
+ export const WRITE_TOOLS = new Set([
47
+ 'bash',
48
+ 'edit',
49
+ 'write_file',
50
+ 'create_file',
51
+ 'delete_file',
52
+ 'rename_file',
53
+ 'move_file',
54
+ ]);
55
+ // =============================================================================
37
56
  // Utilities
38
57
  // =============================================================================
39
58
  /**