@compilr-dev/sdk 0.10.40 → 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.
- package/dist/entitlements/fetch.d.ts +16 -0
- package/dist/entitlements/fetch.js +32 -0
- package/dist/entitlements/index.d.ts +1 -0
- package/dist/entitlements/index.js +1 -0
- package/dist/host/auth-api.d.ts +49 -0
- package/dist/host/auth-api.js +201 -0
- package/dist/host/auth-store.d.ts +36 -0
- package/dist/host/auth-store.js +105 -0
- package/dist/host/auth-types.d.ts +105 -0
- package/dist/host/auth-types.js +11 -0
- package/dist/host/device-flow.d.ts +44 -0
- package/dist/host/device-flow.js +111 -0
- package/dist/host/index.d.ts +13 -0
- package/dist/host/index.js +10 -0
- package/dist/host/settings-schema.d.ts +148 -0
- package/dist/host/settings-schema.js +107 -0
- package/dist/host/token.d.ts +34 -0
- package/dist/host/token.js +56 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +8 -2
- package/dist/permissions.d.ts +8 -0
- package/dist/permissions.js +19 -0
- package/dist/skills/software-skills.js +26 -20
- package/package.json +1 -2
|
@@ -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
|
// =============================================================================
|
package/dist/permissions.d.ts
CHANGED
|
@@ -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).
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
/**
|