@compilr-dev/cli 0.7.6 → 0.8.1
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/auth/api-client.d.ts +8 -121
- package/dist/auth/api-client.js +7 -258
- package/dist/auth/storage.d.ts +7 -49
- package/dist/auth/storage.js +6 -115
- package/dist/commands-v2/handlers/settings.js +11 -1
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/entitlements/index.js +3 -17
- package/dist/index.js +8 -0
- package/dist/repl-v2.js +6 -0
- package/dist/settings/index.js +8 -26
- package/dist/tools/platform-adapter.js +18 -117
- package/dist/ui/overlay-manager.js +22 -2
- package/dist/utils/project-memory.d.ts +8 -43
- package/dist/utils/project-memory.js +7 -114
- package/package.json +4 -4
|
@@ -1,124 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API
|
|
2
|
+
* Auth API client — now a thin re-export of the SDK's shared implementation
|
|
3
|
+
* (`@compilr-dev/sdk` host layer). CLI and Desktop talk to the same compilr.dev
|
|
4
|
+
* Netlify Functions; the client lives in one place.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Note: This is for USER authentication only — users still bring their own LLM API keys.
|
|
8
|
-
*/
|
|
9
|
-
export interface RefreshTokenResponse {
|
|
10
|
-
success: boolean;
|
|
11
|
-
access_token?: string;
|
|
12
|
-
refresh_token?: string;
|
|
13
|
-
expires_in?: number;
|
|
14
|
-
user?: {
|
|
15
|
-
id: string;
|
|
16
|
-
email: string;
|
|
17
|
-
};
|
|
18
|
-
error?: string;
|
|
19
|
-
}
|
|
20
|
-
export interface ProfileData {
|
|
21
|
-
user_id: string;
|
|
22
|
-
email: string;
|
|
23
|
-
account_type: 'free' | 'pro' | 'team' | 'enterprise';
|
|
24
|
-
telemetry_enabled: boolean;
|
|
25
|
-
default_provider: string;
|
|
26
|
-
default_model: string;
|
|
27
|
-
default_tier: string;
|
|
28
|
-
theme: string;
|
|
29
|
-
total_sessions: number;
|
|
30
|
-
total_messages: number;
|
|
31
|
-
first_session_at: string | null;
|
|
32
|
-
last_session_at: string | null;
|
|
33
|
-
created_at: string;
|
|
34
|
-
}
|
|
35
|
-
export interface HeartbeatData {
|
|
36
|
-
session_id: string;
|
|
37
|
-
cli_version: string;
|
|
38
|
-
os: string;
|
|
39
|
-
node_version: string;
|
|
40
|
-
project_id?: number;
|
|
41
|
-
total_messages?: number;
|
|
42
|
-
total_tokens_in?: number;
|
|
43
|
-
total_tokens_out?: number;
|
|
44
|
-
commands_used?: Record<string, number>;
|
|
45
|
-
agents_used?: string[];
|
|
46
|
-
tools_used?: string[];
|
|
47
|
-
llm_provider?: string;
|
|
48
|
-
llm_model?: string;
|
|
49
|
-
}
|
|
50
|
-
export interface DeviceCodeResponse {
|
|
51
|
-
device_code: string;
|
|
52
|
-
user_code: string;
|
|
53
|
-
verification_uri: string;
|
|
54
|
-
verification_uri_complete: string;
|
|
55
|
-
expires_in: number;
|
|
56
|
-
interval: number;
|
|
57
|
-
}
|
|
58
|
-
export interface DeviceTokenResponse {
|
|
59
|
-
access_token: string;
|
|
60
|
-
refresh_token: string;
|
|
61
|
-
token_type: string;
|
|
62
|
-
expires_in: number;
|
|
63
|
-
api_token?: string;
|
|
64
|
-
user: {
|
|
65
|
-
id: string;
|
|
66
|
-
email: string;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export interface DeviceTokenError {
|
|
70
|
-
error: 'authorization_pending' | 'slow_down' | 'expired_token' | 'access_denied' | 'invalid_grant' | 'server_error';
|
|
71
|
-
error_description: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Refresh an access token using a refresh token.
|
|
75
|
-
* Calls the server-side Netlify function which handles Supabase internally.
|
|
76
|
-
*/
|
|
77
|
-
export declare function refreshToken(refreshTokenValue: string): Promise<RefreshTokenResponse>;
|
|
78
|
-
/**
|
|
79
|
-
* Send heartbeat to record session.
|
|
80
|
-
*/
|
|
81
|
-
export declare function sendHeartbeat(accessToken: string, data: HeartbeatData): Promise<{
|
|
82
|
-
success: boolean;
|
|
83
|
-
telemetry_enabled?: boolean;
|
|
84
|
-
error?: string;
|
|
85
|
-
}>;
|
|
86
|
-
/**
|
|
87
|
-
* Get user profile from CLI API.
|
|
88
|
-
*/
|
|
89
|
-
export declare function getProfile(accessToken: string): Promise<{
|
|
90
|
-
success: boolean;
|
|
91
|
-
profile?: ProfileData;
|
|
92
|
-
error?: string;
|
|
93
|
-
}>;
|
|
94
|
-
/**
|
|
95
|
-
* Update user profile via CLI API.
|
|
96
|
-
*/
|
|
97
|
-
export declare function updateProfile(accessToken: string, updates: Partial<Pick<ProfileData, 'telemetry_enabled' | 'default_provider' | 'default_model' | 'default_tier' | 'theme'>>): Promise<{
|
|
98
|
-
success: boolean;
|
|
99
|
-
profile?: ProfileData;
|
|
100
|
-
error?: string;
|
|
101
|
-
}>;
|
|
102
|
-
/**
|
|
103
|
-
* Request a device code for CLI authentication.
|
|
104
|
-
* Returns codes and URLs for the user to authorize.
|
|
105
|
-
*/
|
|
106
|
-
export declare function requestDeviceCode(): Promise<{
|
|
107
|
-
success: boolean;
|
|
108
|
-
data?: DeviceCodeResponse;
|
|
109
|
-
error?: string;
|
|
110
|
-
}>;
|
|
111
|
-
/**
|
|
112
|
-
* Poll for device token after user authorizes.
|
|
113
|
-
* Returns tokens on success, or error status if still pending.
|
|
114
|
-
*/
|
|
115
|
-
export declare function pollDeviceToken(deviceCode: string): Promise<{
|
|
116
|
-
success: boolean;
|
|
117
|
-
data?: DeviceTokenResponse;
|
|
118
|
-
error?: DeviceTokenError;
|
|
119
|
-
networkError?: string;
|
|
120
|
-
}>;
|
|
121
|
-
/**
|
|
122
|
-
* Get the authorization URL for the device flow.
|
|
6
|
+
* The SDK functions read `COMPILR_API_URL` / `COMPILR_WEB_URL` (same defaults as
|
|
7
|
+
* before) and accept an optional endpoints override the CLI doesn't pass, so
|
|
8
|
+
* behavior is unchanged.
|
|
123
9
|
*/
|
|
124
|
-
export
|
|
10
|
+
export { refreshToken, sendHeartbeat, getProfile, updateProfile, requestDeviceCode, pollDeviceToken, getAuthorizationUrl, } from '@compilr-dev/sdk';
|
|
11
|
+
export type { RefreshTokenResponse, ProfileData, HeartbeatData, DeviceCodeResponse, DeviceTokenResponse, DeviceTokenError, } from '@compilr-dev/sdk';
|
package/dist/auth/api-client.js
CHANGED
|
@@ -1,261 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API
|
|
2
|
+
* Auth API client — now a thin re-export of the SDK's shared implementation
|
|
3
|
+
* (`@compilr-dev/sdk` host layer). CLI and Desktop talk to the same compilr.dev
|
|
4
|
+
* Netlify Functions; the client lives in one place.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Note: This is for USER authentication only — users still bring their own LLM API keys.
|
|
8
|
-
*/
|
|
9
|
-
import * as os from 'os';
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// Configuration
|
|
12
|
-
// =============================================================================
|
|
13
|
-
// API endpoint for CLI-specific operations
|
|
14
|
-
// Use COMPILR_API_URL env var for testing against branch previews
|
|
15
|
-
const API_BASE_URL = process.env.COMPILR_API_URL || 'https://compilr.dev/.netlify/functions';
|
|
16
|
-
// Device flow authorization URL
|
|
17
|
-
// Use COMPILR_WEB_URL env var for testing against branch previews
|
|
18
|
-
const WEB_BASE_URL = process.env.COMPILR_WEB_URL || 'https://compilr.dev';
|
|
19
|
-
const AUTHORIZATION_URL = `${WEB_BASE_URL}/cli/authorize`;
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// Token Refresh (via Netlify Function)
|
|
22
|
-
// =============================================================================
|
|
23
|
-
/**
|
|
24
|
-
* Refresh an access token using a refresh token.
|
|
25
|
-
* Calls the server-side Netlify function which handles Supabase internally.
|
|
26
|
-
*/
|
|
27
|
-
export async function refreshToken(refreshTokenValue) {
|
|
28
|
-
try {
|
|
29
|
-
const response = await fetch(`${API_BASE_URL}/cli-refresh-token`, {
|
|
30
|
-
method: 'POST',
|
|
31
|
-
headers: {
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
},
|
|
34
|
-
body: JSON.stringify({ refresh_token: refreshTokenValue }),
|
|
35
|
-
});
|
|
36
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
37
|
-
if (!contentType.includes('application/json')) {
|
|
38
|
-
return { success: false, error: `Server returned non-JSON response (${String(response.status)})` };
|
|
39
|
-
}
|
|
40
|
-
const result = await response.json();
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
return {
|
|
43
|
-
success: false,
|
|
44
|
-
error: result.error ?? `HTTP ${String(response.status)}`,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
success: true,
|
|
49
|
-
access_token: result.access_token,
|
|
50
|
-
refresh_token: result.refresh_token,
|
|
51
|
-
expires_in: result.expires_in,
|
|
52
|
-
user: result.user,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
return {
|
|
57
|
-
success: false,
|
|
58
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// =============================================================================
|
|
63
|
-
// CLI API Operations (against our Netlify Functions)
|
|
64
|
-
// =============================================================================
|
|
65
|
-
/**
|
|
66
|
-
* Send heartbeat to record session.
|
|
67
|
-
*/
|
|
68
|
-
export async function sendHeartbeat(accessToken, data) {
|
|
69
|
-
try {
|
|
70
|
-
const response = await fetch(`${API_BASE_URL}/cli-heartbeat`, {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: {
|
|
73
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
74
|
-
'Content-Type': 'application/json',
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify(data),
|
|
77
|
-
});
|
|
78
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
79
|
-
if (!contentType.includes('application/json')) {
|
|
80
|
-
return { success: false, error: `Server returned non-JSON response (${String(response.status)})` };
|
|
81
|
-
}
|
|
82
|
-
const result = await response.json();
|
|
83
|
-
if (!response.ok) {
|
|
84
|
-
return {
|
|
85
|
-
success: false,
|
|
86
|
-
error: result.error ?? `HTTP ${String(response.status)}`,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
success: result.success ?? true,
|
|
91
|
-
telemetry_enabled: result.telemetry_enabled,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
return {
|
|
96
|
-
success: false,
|
|
97
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Get user profile from CLI API.
|
|
103
|
-
*/
|
|
104
|
-
export async function getProfile(accessToken) {
|
|
105
|
-
try {
|
|
106
|
-
const response = await fetch(`${API_BASE_URL}/cli-profile`, {
|
|
107
|
-
method: 'GET',
|
|
108
|
-
headers: {
|
|
109
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
113
|
-
if (!contentType.includes('application/json')) {
|
|
114
|
-
return { success: false, error: `Server returned non-JSON response (${String(response.status)})` };
|
|
115
|
-
}
|
|
116
|
-
const result = await response.json();
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
return {
|
|
119
|
-
success: false,
|
|
120
|
-
error: result.error ?? `HTTP ${String(response.status)}`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
success: true,
|
|
125
|
-
profile: result,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
return {
|
|
130
|
-
success: false,
|
|
131
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Update user profile via CLI API.
|
|
137
|
-
*/
|
|
138
|
-
export async function updateProfile(accessToken, updates) {
|
|
139
|
-
try {
|
|
140
|
-
const response = await fetch(`${API_BASE_URL}/cli-profile`, {
|
|
141
|
-
method: 'PATCH',
|
|
142
|
-
headers: {
|
|
143
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
144
|
-
'Content-Type': 'application/json',
|
|
145
|
-
},
|
|
146
|
-
body: JSON.stringify(updates),
|
|
147
|
-
});
|
|
148
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
149
|
-
if (!contentType.includes('application/json')) {
|
|
150
|
-
return { success: false, error: `Server returned non-JSON response (${String(response.status)})` };
|
|
151
|
-
}
|
|
152
|
-
const result = await response.json();
|
|
153
|
-
if (!response.ok) {
|
|
154
|
-
return {
|
|
155
|
-
success: false,
|
|
156
|
-
error: result.error ?? `HTTP ${String(response.status)}`,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
success: result.success ?? true,
|
|
161
|
-
profile: result.profile,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// =============================================================================
|
|
172
|
-
// Device Flow Authentication
|
|
173
|
-
// =============================================================================
|
|
174
|
-
/**
|
|
175
|
-
* Request a device code for CLI authentication.
|
|
176
|
-
* Returns codes and URLs for the user to authorize.
|
|
177
|
-
*/
|
|
178
|
-
export async function requestDeviceCode() {
|
|
179
|
-
try {
|
|
180
|
-
const url = `${API_BASE_URL}/cli-device-code`;
|
|
181
|
-
const response = await fetch(url, {
|
|
182
|
-
method: 'POST',
|
|
183
|
-
headers: {
|
|
184
|
-
'Content-Type': 'application/json',
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
188
|
-
if (!contentType.includes('application/json')) {
|
|
189
|
-
return {
|
|
190
|
-
success: false,
|
|
191
|
-
error: `Non-JSON response (${String(response.status)}) from: ${url}`,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (!response.ok) {
|
|
195
|
-
const result = await response.json();
|
|
196
|
-
return {
|
|
197
|
-
success: false,
|
|
198
|
-
error: result.error ?? `HTTP ${String(response.status)}`,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
const data = await response.json();
|
|
202
|
-
return { success: true, data };
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
return {
|
|
206
|
-
success: false,
|
|
207
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Poll for device token after user authorizes.
|
|
213
|
-
* Returns tokens on success, or error status if still pending.
|
|
214
|
-
*/
|
|
215
|
-
export async function pollDeviceToken(deviceCode) {
|
|
216
|
-
try {
|
|
217
|
-
const response = await fetch(`${API_BASE_URL}/cli-device-token`, {
|
|
218
|
-
method: 'POST',
|
|
219
|
-
headers: {
|
|
220
|
-
'Content-Type': 'application/json',
|
|
221
|
-
},
|
|
222
|
-
body: JSON.stringify({
|
|
223
|
-
device_code: deviceCode,
|
|
224
|
-
hostname: os.hostname(),
|
|
225
|
-
}),
|
|
226
|
-
});
|
|
227
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
228
|
-
if (!contentType.includes('application/json')) {
|
|
229
|
-
return {
|
|
230
|
-
success: false,
|
|
231
|
-
networkError: `Server returned non-JSON response (${String(response.status)})`,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
const result = await response.json();
|
|
235
|
-
if (!response.ok) {
|
|
236
|
-
return {
|
|
237
|
-
success: false,
|
|
238
|
-
error: result,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
success: true,
|
|
243
|
-
data: result,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
return {
|
|
248
|
-
success: false,
|
|
249
|
-
networkError: error instanceof Error ? error.message : 'Network error',
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Get the authorization URL for the device flow.
|
|
6
|
+
* The SDK functions read `COMPILR_API_URL` / `COMPILR_WEB_URL` (same defaults as
|
|
7
|
+
* before) and accept an optional endpoints override the CLI doesn't pass, so
|
|
8
|
+
* behavior is unchanged.
|
|
255
9
|
*/
|
|
256
|
-
export
|
|
257
|
-
if (userCode) {
|
|
258
|
-
return `${AUTHORIZATION_URL}?code=${userCode}`;
|
|
259
|
-
}
|
|
260
|
-
return AUTHORIZATION_URL;
|
|
261
|
-
}
|
|
10
|
+
export { refreshToken, sendHeartbeat, getProfile, updateProfile, requestDeviceCode, pollDeviceToken, getAuthorizationUrl, } from '@compilr-dev/sdk';
|
package/dist/auth/storage.d.ts
CHANGED
|
@@ -1,52 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth
|
|
2
|
+
* Auth storage — now a thin re-export of the SDK's shared implementation
|
|
3
|
+
* (`@compilr-dev/sdk` host layer). CLI and Desktop persist the same
|
|
4
|
+
* `~/.compilr-dev/auth.json`; the logic lives in one place.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Security:
|
|
8
|
-
* - File permissions set to 0600 (owner read/write only)
|
|
9
|
-
* - Tokens are JWTs from Supabase Auth
|
|
10
|
-
*/
|
|
11
|
-
export interface AuthUser {
|
|
12
|
-
id: string;
|
|
13
|
-
email: string;
|
|
14
|
-
createdAt: string;
|
|
15
|
-
}
|
|
16
|
-
export interface AuthSession {
|
|
17
|
-
accessToken: string;
|
|
18
|
-
refreshToken: string;
|
|
19
|
-
expiresAt: string;
|
|
20
|
-
apiToken?: string;
|
|
21
|
-
}
|
|
22
|
-
export interface AuthData {
|
|
23
|
-
version: number;
|
|
24
|
-
user: AuthUser;
|
|
25
|
-
session: AuthSession;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Load auth data from disk.
|
|
29
|
-
* Returns null if file doesn't exist or is invalid.
|
|
30
|
-
*/
|
|
31
|
-
export declare function loadAuthData(): AuthData | null;
|
|
32
|
-
/**
|
|
33
|
-
* Save auth data to disk with secure permissions.
|
|
34
|
-
*/
|
|
35
|
-
export declare function saveAuthData(data: AuthData): void;
|
|
36
|
-
/**
|
|
37
|
-
* Clear auth data (logout).
|
|
38
|
-
*/
|
|
39
|
-
export declare function clearAuthData(): void;
|
|
40
|
-
/**
|
|
41
|
-
* Update just the session tokens (after refresh).
|
|
42
|
-
*/
|
|
43
|
-
export declare function updateSession(session: AuthSession): boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Check if auth data exists (quick check without loading).
|
|
46
|
-
*/
|
|
47
|
-
export declare function hasAuthData(): boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Check if stored auth file has insecure permissions.
|
|
50
|
-
* Returns warning message if insecure, null if OK.
|
|
6
|
+
* The SDK functions default to `~/.compilr-dev` (== this module's historical
|
|
7
|
+
* CONFIG_DIR), so behavior is identical — no `dir` argument is passed.
|
|
51
8
|
*/
|
|
52
|
-
export
|
|
9
|
+
export { loadAuthData, saveAuthData, clearAuthData, updateSession, hasAuthData, checkAuthFilePermissions, } from '@compilr-dev/sdk';
|
|
10
|
+
export type { AuthUser, AuthSession, AuthData } from '@compilr-dev/sdk';
|
package/dist/auth/storage.js
CHANGED
|
@@ -1,118 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth
|
|
2
|
+
* Auth storage — now a thin re-export of the SDK's shared implementation
|
|
3
|
+
* (`@compilr-dev/sdk` host layer). CLI and Desktop persist the same
|
|
4
|
+
* `~/.compilr-dev/auth.json`; the logic lives in one place.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Security:
|
|
8
|
-
* - File permissions set to 0600 (owner read/write only)
|
|
9
|
-
* - Tokens are JWTs from Supabase Auth
|
|
10
|
-
*/
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
import * as path from 'path';
|
|
13
|
-
import * as os from 'os';
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// Constants
|
|
16
|
-
// =============================================================================
|
|
17
|
-
const CONFIG_DIR = path.join(os.homedir(), '.compilr-dev');
|
|
18
|
-
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
19
|
-
// =============================================================================
|
|
20
|
-
// Storage Operations
|
|
21
|
-
// =============================================================================
|
|
22
|
-
/**
|
|
23
|
-
* Load auth data from disk.
|
|
24
|
-
* Returns null if file doesn't exist or is invalid.
|
|
25
|
-
*/
|
|
26
|
-
export function loadAuthData() {
|
|
27
|
-
try {
|
|
28
|
-
if (!fs.existsSync(AUTH_FILE)) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
const data = fs.readFileSync(AUTH_FILE, 'utf-8');
|
|
32
|
-
const parsed = JSON.parse(data);
|
|
33
|
-
// Validate required fields
|
|
34
|
-
if (typeof parsed.version !== 'number' ||
|
|
35
|
-
typeof parsed.user !== 'object' ||
|
|
36
|
-
!parsed.user ||
|
|
37
|
-
typeof parsed.session !== 'object' ||
|
|
38
|
-
!parsed.session) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
return parsed;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// File doesn't exist or is corrupted
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Save auth data to disk with secure permissions.
|
|
50
|
-
*/
|
|
51
|
-
export function saveAuthData(data) {
|
|
52
|
-
// Ensure config directory exists
|
|
53
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
54
|
-
// Write with restrictive permissions
|
|
55
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), {
|
|
56
|
-
encoding: 'utf-8',
|
|
57
|
-
mode: 0o600, // Owner read/write only
|
|
58
|
-
});
|
|
59
|
-
// Verify permissions (fix if system ignored mode)
|
|
60
|
-
try {
|
|
61
|
-
fs.chmodSync(AUTH_FILE, 0o600);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Ignore chmod errors (e.g., on Windows)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Clear auth data (logout).
|
|
69
|
-
*/
|
|
70
|
-
export function clearAuthData() {
|
|
71
|
-
try {
|
|
72
|
-
if (fs.existsSync(AUTH_FILE)) {
|
|
73
|
-
fs.unlinkSync(AUTH_FILE);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// Ignore errors during cleanup
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Update just the session tokens (after refresh).
|
|
82
|
-
*/
|
|
83
|
-
export function updateSession(session) {
|
|
84
|
-
const data = loadAuthData();
|
|
85
|
-
if (!data) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
data.session = session;
|
|
89
|
-
saveAuthData(data);
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Check if auth data exists (quick check without loading).
|
|
94
|
-
*/
|
|
95
|
-
export function hasAuthData() {
|
|
96
|
-
return fs.existsSync(AUTH_FILE);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Check if stored auth file has insecure permissions.
|
|
100
|
-
* Returns warning message if insecure, null if OK.
|
|
6
|
+
* The SDK functions default to `~/.compilr-dev` (== this module's historical
|
|
7
|
+
* CONFIG_DIR), so behavior is identical — no `dir` argument is passed.
|
|
101
8
|
*/
|
|
102
|
-
export
|
|
103
|
-
try {
|
|
104
|
-
if (!fs.existsSync(AUTH_FILE)) {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
const stats = fs.statSync(AUTH_FILE);
|
|
108
|
-
const mode = stats.mode & 0o777; // Get permission bits
|
|
109
|
-
// Check if group or others have any access
|
|
110
|
-
if (mode & 0o077) {
|
|
111
|
-
return `Warning: Auth file has insecure permissions (${mode.toString(8)}). Consider running: chmod 600 ${AUTH_FILE}`;
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
9
|
+
export { loadAuthData, saveAuthData, clearAuthData, updateSession, hasAuthData, checkAuthFilePermissions, } from '@compilr-dev/sdk';
|
|
@@ -25,7 +25,13 @@ export const themeCommand = {
|
|
|
25
25
|
],
|
|
26
26
|
async execute(_args, ctx) {
|
|
27
27
|
const themeOverlay = new ThemeOverlayV2();
|
|
28
|
-
await ctx.ui.showOverlay(themeOverlay);
|
|
28
|
+
const result = await ctx.ui.showOverlay(themeOverlay);
|
|
29
|
+
// The overlay updates the theme registry, but the input prompt prefix (the
|
|
30
|
+
// accent-colored "compilr>") is cached and must be rebuilt to pick up the new
|
|
31
|
+
// accent color — otherwise it stays stale until the next restart.
|
|
32
|
+
if (result?.themeChanged) {
|
|
33
|
+
ctx.ui.refreshPrompt();
|
|
34
|
+
}
|
|
29
35
|
return true;
|
|
30
36
|
},
|
|
31
37
|
};
|
|
@@ -78,6 +84,10 @@ export const configCommand = {
|
|
|
78
84
|
messageCount,
|
|
79
85
|
});
|
|
80
86
|
const result = await ctx.ui.showOverlay(configOverlay);
|
|
87
|
+
// The config overlay can change the theme via its sub-overlay without
|
|
88
|
+
// surfacing it in the result, so rebuild the cached prompt prefix
|
|
89
|
+
// unconditionally on close — refreshPrompt() reads colors live and is cheap.
|
|
90
|
+
ctx.ui.refreshPrompt();
|
|
81
91
|
if (result?.settingsChanged) {
|
|
82
92
|
// Sync TerminalUI config with persisted settings
|
|
83
93
|
const updated = getSettings();
|
|
Binary file
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Uses encrypted file for offline backup
|
|
7
7
|
* - Warms cache on startup
|
|
8
8
|
*/
|
|
9
|
-
import { EntitlementCache } from '@compilr-dev/sdk';
|
|
9
|
+
import { EntitlementCache, fetchEntitlements, } from '@compilr-dev/sdk';
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto';
|
|
@@ -60,23 +60,9 @@ const cliStore = {
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
|
-
// ─── Fetch Function ─────────────────────────────────────────────────────────
|
|
64
|
-
async function fetchEntitlements(getToken) {
|
|
65
|
-
const token = await getToken();
|
|
66
|
-
const apiUrl = process.env['COMPILR_API_URL'] ?? 'https://compilr.dev/.netlify/functions';
|
|
67
|
-
const resp = await fetch(`${apiUrl}/cli-entitlements`, {
|
|
68
|
-
method: 'GET',
|
|
69
|
-
headers: {
|
|
70
|
-
'Authorization': `Bearer ${token}`,
|
|
71
|
-
'Content-Type': 'application/json',
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
if (!resp.ok) {
|
|
75
|
-
throw new Error(`Entitlements fetch failed: ${String(resp.status)}`);
|
|
76
|
-
}
|
|
77
|
-
return await resp.json();
|
|
78
|
-
}
|
|
79
63
|
// ─── Singleton ──────────────────────────────────────────────────────────────
|
|
64
|
+
// Fetch lives in the SDK (shared with Desktop). `getToken` returns Promise<string>
|
|
65
|
+
// here, which satisfies the SDK's Promise<string | null> contract.
|
|
80
66
|
import { DailyCounter } from '@compilr-dev/sdk';
|
|
81
67
|
let cacheInstance = null;
|
|
82
68
|
/** Daily message counter — tracks agent LLM calls per day */
|
package/dist/index.js
CHANGED
|
@@ -885,6 +885,14 @@ async function main() {
|
|
|
885
885
|
// Show login overlay if not authenticated
|
|
886
886
|
showLogin,
|
|
887
887
|
});
|
|
888
|
+
// Always restore the hardware cursor on exit. Overlays/menus hide it while
|
|
889
|
+
// showing nav-only screens; without this, quitting (incl. Ctrl+C, which
|
|
890
|
+
// routes through process.exit → 'exit') from such a screen would leave the
|
|
891
|
+
// user's terminal cursor invisible.
|
|
892
|
+
process.on('exit', () => {
|
|
893
|
+
if (process.stdout.isTTY)
|
|
894
|
+
process.stdout.write('\x1b[?25h');
|
|
895
|
+
});
|
|
888
896
|
// Handle Ctrl+C gracefully
|
|
889
897
|
process.on('SIGINT', () => {
|
|
890
898
|
// Flush episode recorder before exit
|
package/dist/repl-v2.js
CHANGED
|
@@ -3719,6 +3719,12 @@ export class ReplV2 {
|
|
|
3719
3719
|
// Only run main() when executed directly (not when imported)
|
|
3720
3720
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
3721
3721
|
const repl = new ReplV2();
|
|
3722
|
+
// Restore the hardware cursor on exit (overlays/menus hide it for nav-only
|
|
3723
|
+
// screens; don't leave the user's terminal cursor invisible after quit).
|
|
3724
|
+
process.on('exit', () => {
|
|
3725
|
+
if (process.stdout.isTTY)
|
|
3726
|
+
process.stdout.write('\x1b[?25h');
|
|
3727
|
+
});
|
|
3722
3728
|
// Handle SIGINT — restore raw mode before exit so the parent shell
|
|
3723
3729
|
// doesn't inherit a broken terminal state.
|
|
3724
3730
|
process.on('SIGINT', () => {
|
package/dist/settings/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { log } from '../foundation/logger.js';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as os from 'os';
|
|
11
|
-
import { DEFAULT_PERMISSION_RULES, findMatchingRule as sdkFindMatchingRule, } from '@compilr-dev/sdk';
|
|
11
|
+
import { DEFAULT_PERMISSION_RULES, findMatchingRule as sdkFindMatchingRule, migrateRawSettings, } from '@compilr-dev/sdk';
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// Constants
|
|
14
14
|
// =============================================================================
|
|
@@ -103,32 +103,14 @@ export function getSettings() {
|
|
|
103
103
|
if (settingsCache) {
|
|
104
104
|
return settingsCache;
|
|
105
105
|
}
|
|
106
|
+
// Value migrations live in the SDK (migrateRawSettings) so CLI and Desktop
|
|
107
|
+
// normalize the shared settings.json identically. It's pure, idempotent, and
|
|
108
|
+
// preserves unknown keys (incl. Desktop-only fields). We run it on the raw
|
|
109
|
+
// file, then merge over CLI defaults.
|
|
106
110
|
const stored = loadFromDisk();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
stored.verbosity = 'verbose';
|
|
111
|
-
needsSave = true;
|
|
112
|
-
}
|
|
113
|
-
settingsCache = { ...DEFAULT_SETTINGS, ...stored };
|
|
114
|
-
if (needsSave) {
|
|
115
|
-
saveToDisk(settingsCache);
|
|
116
|
-
}
|
|
117
|
-
// Migration: convert deprecated 'cwd' to 'off'
|
|
118
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
-
if (settingsCache.projectStartup === 'cwd') {
|
|
120
|
-
settingsCache.projectStartup = 'off';
|
|
121
|
-
saveToDisk(settingsCache);
|
|
122
|
-
}
|
|
123
|
-
// Migration: convert old permission mode values
|
|
124
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
-
const oldPermMode = settingsCache.permissionMode;
|
|
126
|
-
if (oldPermMode === 'default' || oldPermMode === 'accept-edits') {
|
|
127
|
-
settingsCache.permissionMode = 'normal';
|
|
128
|
-
saveToDisk(settingsCache);
|
|
129
|
-
}
|
|
130
|
-
else if (oldPermMode === 'dont-ask') {
|
|
131
|
-
settingsCache.permissionMode = 'auto-accept';
|
|
111
|
+
const { settings: migrated, changed } = migrateRawSettings(stored);
|
|
112
|
+
settingsCache = { ...DEFAULT_SETTINGS, ...migrated };
|
|
113
|
+
if (changed) {
|
|
132
114
|
saveToDisk(settingsCache);
|
|
133
115
|
}
|
|
134
116
|
return settingsCache;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* wires CLI-specific hooks and services (anchors, artifacts, episodes),
|
|
6
6
|
* and delegates all 32 platform tool definitions to the SDK.
|
|
7
7
|
*/
|
|
8
|
-
import { createPlatformTools, createSQLiteRepositories, ProjectAnchorStore, } from '@compilr-dev/sdk';
|
|
8
|
+
import { createPlatformTools, createSQLiteRepositories, ProjectAnchorStore, FileArtifactService, } from '@compilr-dev/sdk';
|
|
9
9
|
import { createAllFactoryTools } from '@compilr-dev/factory';
|
|
10
10
|
import { getDb } from '../db/index.js';
|
|
11
11
|
import { getDataPath, getSessionsPath } from '../settings/paths.js';
|
|
@@ -16,7 +16,7 @@ import { getCurrentProject, setCurrentProject } from './project-db.js';
|
|
|
16
16
|
import { awardFirstProject, awardWorkItemCompletion } from '../games/coins.js';
|
|
17
17
|
import { getActiveSharedContext } from '@compilr-dev/sdk';
|
|
18
18
|
import { setAnchorStore } from '../anchors/index.js';
|
|
19
|
-
import {
|
|
19
|
+
import { recordTeamActivity, getSessionsPath as getCheckpointerSessionsPath } from '../multi-agent/index.js';
|
|
20
20
|
import { getGlobalEpisodeStore } from '../episodes/index.js';
|
|
21
21
|
// =============================================================================
|
|
22
22
|
// Repositories — SDK's SQLite implementations, backed by CLI's existing DB
|
|
@@ -63,123 +63,24 @@ const anchorService = anchorStore.toService();
|
|
|
63
63
|
// =============================================================================
|
|
64
64
|
// Artifact Service — wraps ArtifactStore from TeamCheckpointer
|
|
65
65
|
// =============================================================================
|
|
66
|
+
// Artifacts — file-backed service shared with Desktop via the SDK
|
|
67
|
+
// (FileArtifactService), pointed at the same sessions dir the checkpointer uses
|
|
68
|
+
// so artifact_* tools and team hydration read/write the same artifacts.json.
|
|
69
|
+
// We wrap save() to record team activity attributed to the ACTING agent
|
|
70
|
+
// (input.agent), which differs from the artifact's original creator on updates.
|
|
71
|
+
const baseArtifactService = new FileArtifactService({
|
|
72
|
+
sessionsDir: getCheckpointerSessionsPath(),
|
|
73
|
+
getProjectId: () => getCurrentProject()?.id ?? null,
|
|
74
|
+
});
|
|
66
75
|
const artifactService = {
|
|
67
|
-
save: (input) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const agentId = input.agent ?? 'default';
|
|
72
|
-
const existing = store.getByName(input.name);
|
|
73
|
-
if (existing) {
|
|
74
|
-
const updated = store.update(existing.id, {
|
|
75
|
-
content: input.content,
|
|
76
|
-
summary: input.summary,
|
|
77
|
-
type: input.type,
|
|
78
|
-
});
|
|
79
|
-
if (!updated)
|
|
80
|
-
throw new Error(`Failed to update artifact "${input.name}"`);
|
|
81
|
-
getTeamCheckpointer().saveArtifactStore(currentProject?.id ?? null, store);
|
|
82
|
-
recordTeamActivity(agentId, 'artifact_updated', `updated artifact "${updated.name}"`);
|
|
83
|
-
const artifact = {
|
|
84
|
-
id: updated.id,
|
|
85
|
-
name: updated.name,
|
|
86
|
-
agent: updated.agent,
|
|
87
|
-
type: updated.type,
|
|
88
|
-
content: updated.content,
|
|
89
|
-
summary: updated.summary,
|
|
90
|
-
version: updated.version,
|
|
91
|
-
createdAt: updated.createdAt,
|
|
92
|
-
updatedAt: updated.updatedAt,
|
|
93
|
-
};
|
|
94
|
-
return Promise.resolve({ artifact, isUpdate: true });
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
const created = store.create({
|
|
98
|
-
name: input.name,
|
|
99
|
-
agent: agentId,
|
|
100
|
-
type: input.type,
|
|
101
|
-
content: input.content,
|
|
102
|
-
summary: input.summary,
|
|
103
|
-
});
|
|
104
|
-
getTeamCheckpointer().saveArtifactStore(currentProject?.id ?? null, store);
|
|
105
|
-
recordTeamActivity(agentId, 'artifact_created', `created artifact "${created.name}"`);
|
|
106
|
-
const artifact = {
|
|
107
|
-
id: created.id,
|
|
108
|
-
name: created.name,
|
|
109
|
-
agent: created.agent,
|
|
110
|
-
type: created.type,
|
|
111
|
-
content: created.content,
|
|
112
|
-
summary: created.summary,
|
|
113
|
-
version: created.version,
|
|
114
|
-
createdAt: created.createdAt,
|
|
115
|
-
updatedAt: created.updatedAt,
|
|
116
|
-
};
|
|
117
|
-
return Promise.resolve({ artifact, isUpdate: false });
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
getByName: (name) => {
|
|
121
|
-
const currentProject = getCurrentProject();
|
|
122
|
-
const store = getTeamCheckpointer().getArtifactStore(currentProject?.id ?? null);
|
|
123
|
-
const artifact = store.getByName(name);
|
|
124
|
-
if (!artifact)
|
|
125
|
-
return Promise.resolve(null);
|
|
126
|
-
const result = {
|
|
127
|
-
id: artifact.id,
|
|
128
|
-
name: artifact.name,
|
|
129
|
-
agent: artifact.agent,
|
|
130
|
-
type: artifact.type,
|
|
131
|
-
content: artifact.content,
|
|
132
|
-
summary: artifact.summary,
|
|
133
|
-
version: artifact.version,
|
|
134
|
-
createdAt: artifact.createdAt,
|
|
135
|
-
updatedAt: artifact.updatedAt,
|
|
136
|
-
};
|
|
137
|
-
return Promise.resolve(result);
|
|
138
|
-
},
|
|
139
|
-
list: (options) => {
|
|
140
|
-
const currentProject = getCurrentProject();
|
|
141
|
-
const store = getTeamCheckpointer().getArtifactStore(currentProject?.id ?? null);
|
|
142
|
-
let items = options?.search ? store.search(options.search) : store.list();
|
|
143
|
-
if (options?.type) {
|
|
144
|
-
items = items.filter(a => a.type === options.type);
|
|
145
|
-
}
|
|
146
|
-
if (options?.agent) {
|
|
147
|
-
items = items.filter(a => a.agent === options.agent);
|
|
148
|
-
}
|
|
149
|
-
const summaries = items.map(a => ({
|
|
150
|
-
id: a.id,
|
|
151
|
-
name: a.name,
|
|
152
|
-
agent: a.agent,
|
|
153
|
-
type: a.type,
|
|
154
|
-
summary: a.summary,
|
|
155
|
-
version: a.version,
|
|
156
|
-
updatedAt: a.updatedAt,
|
|
157
|
-
}));
|
|
158
|
-
return Promise.resolve(summaries);
|
|
159
|
-
},
|
|
160
|
-
delete: (name) => {
|
|
161
|
-
const currentProject = getCurrentProject();
|
|
162
|
-
const store = getTeamCheckpointer().getArtifactStore(currentProject?.id ?? null);
|
|
163
|
-
const artifact = store.getByName(name);
|
|
164
|
-
if (!artifact)
|
|
165
|
-
return Promise.resolve(null);
|
|
166
|
-
const deleted = store.delete(artifact.id);
|
|
167
|
-
if (!deleted)
|
|
168
|
-
return Promise.resolve(null);
|
|
169
|
-
getTeamCheckpointer().saveArtifactStore(currentProject?.id ?? null, store);
|
|
170
|
-
const result = {
|
|
171
|
-
id: artifact.id,
|
|
172
|
-
name: artifact.name,
|
|
173
|
-
agent: artifact.agent,
|
|
174
|
-
type: artifact.type,
|
|
175
|
-
content: artifact.content,
|
|
176
|
-
summary: artifact.summary,
|
|
177
|
-
version: artifact.version,
|
|
178
|
-
createdAt: artifact.createdAt,
|
|
179
|
-
updatedAt: artifact.updatedAt,
|
|
180
|
-
};
|
|
181
|
-
return Promise.resolve(result);
|
|
76
|
+
save: async (input) => {
|
|
77
|
+
const result = await baseArtifactService.save(input);
|
|
78
|
+
recordTeamActivity(input.agent ?? 'default', result.isUpdate ? 'artifact_updated' : 'artifact_created', `${result.isUpdate ? 'updated' : 'created'} artifact "${result.artifact.name}"`);
|
|
79
|
+
return result;
|
|
182
80
|
},
|
|
81
|
+
getByName: (name) => baseArtifactService.getByName(name),
|
|
82
|
+
list: (options) => baseArtifactService.list(options),
|
|
83
|
+
delete: (name) => baseArtifactService.delete(name),
|
|
183
84
|
};
|
|
184
85
|
// =============================================================================
|
|
185
86
|
// Episode Service — wraps FileEpisodeStore
|
|
@@ -176,6 +176,9 @@ export class OverlayManager {
|
|
|
176
176
|
process.stdout.write('\n');
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
// Only show the hardware cursor for overlays that actually take text input
|
|
180
|
+
// (they provide a cursorPosition). Nav-only overlays (menus, dashboards,
|
|
181
|
+
// selection lists) hide it so it doesn't blink, parked, below the screen.
|
|
179
182
|
if (content.cursorPosition) {
|
|
180
183
|
const { line, column } = content.cursorPosition;
|
|
181
184
|
const linesFromEnd = paddedLines.length - 1 - line;
|
|
@@ -183,8 +186,11 @@ export class OverlayManager {
|
|
|
183
186
|
ttyWrite(`\x1b[${String(linesFromEnd)}A`);
|
|
184
187
|
}
|
|
185
188
|
ttyWrite(`\x1b[${String(column)}G`);
|
|
189
|
+
terminal.showCursor();
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
terminal.hideCursor();
|
|
186
193
|
}
|
|
187
|
-
terminal.showCursor();
|
|
188
194
|
}
|
|
189
195
|
renderInlineOverlay(content, termWidth) {
|
|
190
196
|
this.clearOverlayRender();
|
|
@@ -206,7 +212,21 @@ export class OverlayManager {
|
|
|
206
212
|
for (const line of paddedLines) {
|
|
207
213
|
process.stdout.write(line + '\n');
|
|
208
214
|
}
|
|
209
|
-
|
|
215
|
+
// Same rule as fullscreen: cursor only for text-input overlays. Inline
|
|
216
|
+
// writes a trailing newline after every line, so the cursor sits one row
|
|
217
|
+
// below the last content line (hence no -1 in linesFromEnd).
|
|
218
|
+
if (content.cursorPosition) {
|
|
219
|
+
const { line, column } = content.cursorPosition;
|
|
220
|
+
const linesFromEnd = paddedLines.length - line;
|
|
221
|
+
if (linesFromEnd > 0) {
|
|
222
|
+
ttyWrite(`\x1b[${String(linesFromEnd)}A`);
|
|
223
|
+
}
|
|
224
|
+
ttyWrite(`\x1b[${String(column)}G`);
|
|
225
|
+
terminal.showCursor();
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
terminal.hideCursor();
|
|
229
|
+
}
|
|
210
230
|
}
|
|
211
231
|
// ===========================================================================
|
|
212
232
|
// Action processing
|
|
@@ -1,47 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project Memory Loader
|
|
2
|
+
* Project Memory Loader (CLI)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
export interface ProjectMemoryResult {
|
|
8
|
-
/** Whether a memory file was found */
|
|
9
|
-
found: boolean;
|
|
10
|
-
/** The loaded content (may be truncated) */
|
|
11
|
-
content: string;
|
|
12
|
-
/** Path to the file that was loaded */
|
|
13
|
-
filePath: string | null;
|
|
14
|
-
/** Original file size in bytes */
|
|
15
|
-
originalSize: number;
|
|
16
|
-
/** Whether the content was truncated */
|
|
17
|
-
truncated: boolean;
|
|
18
|
-
/** Estimated token count (chars / 4) */
|
|
19
|
-
estimatedTokens: number;
|
|
20
|
-
}
|
|
21
|
-
export interface ProjectMemoryOptions {
|
|
22
|
-
/** Maximum content size in bytes before truncation (default: 100KB) */
|
|
23
|
-
maxSize?: number;
|
|
24
|
-
/** Size threshold for warning (default: 30KB) */
|
|
25
|
-
warnSize?: number;
|
|
26
|
-
/** Custom search directory (default: process.cwd()) */
|
|
27
|
-
cwd?: string;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Load project memory from COMPILR.md or CLAUDE.md
|
|
4
|
+
* Now a thin re-export of the SDK's shared loader (loadProjectContextFiles):
|
|
5
|
+
* priority search over COMPILR.md / .compilr/instructions.md / CLAUDE.md /
|
|
6
|
+
* .claude/instructions.md with size-guard truncation. Shared with Desktop.
|
|
31
7
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
export declare function loadProjectMemory(options?: ProjectMemoryOptions): ProjectMemoryResult;
|
|
36
|
-
/**
|
|
37
|
-
* Check if a project memory file exists without loading it
|
|
38
|
-
*/
|
|
39
|
-
export declare function hasProjectMemory(cwd?: string): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Get the size category for display purposes
|
|
42
|
-
*/
|
|
43
|
-
export declare function getSizeCategory(bytes: number): 'small' | 'medium' | 'large';
|
|
44
|
-
/**
|
|
45
|
-
* Format bytes as human-readable string
|
|
8
|
+
* The previous CLI-only display helpers (hasProjectMemory / getSizeCategory /
|
|
9
|
+
* formatBytes) had no callers and were removed.
|
|
46
10
|
*/
|
|
47
|
-
export
|
|
11
|
+
export { loadProjectContextFiles as loadProjectMemory } from '@compilr-dev/sdk';
|
|
12
|
+
export type { ProjectContextResult as ProjectMemoryResult, LoadProjectContextOptions as ProjectMemoryOptions, } from '@compilr-dev/sdk';
|
|
@@ -1,118 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project Memory Loader
|
|
2
|
+
* Project Memory Loader (CLI)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
import * as fs from 'fs';
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
import { estimateTokens } from './token-tracker.js';
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// Constants
|
|
12
|
-
// =============================================================================
|
|
13
|
-
/** Files to search for, in priority order */
|
|
14
|
-
const MEMORY_FILES = [
|
|
15
|
-
'COMPILR.md',
|
|
16
|
-
'.compilr/instructions.md',
|
|
17
|
-
'CLAUDE.md',
|
|
18
|
-
'.claude/instructions.md',
|
|
19
|
-
];
|
|
20
|
-
/** Default max size: 100KB */
|
|
21
|
-
const DEFAULT_MAX_SIZE = 100 * 1024;
|
|
22
|
-
/** Default warn size: 30KB */
|
|
23
|
-
const DEFAULT_WARN_SIZE = 30 * 1024;
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Main Function
|
|
26
|
-
// =============================================================================
|
|
27
|
-
/**
|
|
28
|
-
* Load project memory from COMPILR.md or CLAUDE.md
|
|
4
|
+
* Now a thin re-export of the SDK's shared loader (loadProjectContextFiles):
|
|
5
|
+
* priority search over COMPILR.md / .compilr/instructions.md / CLAUDE.md /
|
|
6
|
+
* .claude/instructions.md with size-guard truncation. Shared with Desktop.
|
|
29
7
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
33
|
-
export function loadProjectMemory(options = {}) {
|
|
34
|
-
const cwd = options.cwd ?? process.cwd();
|
|
35
|
-
const maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
|
|
36
|
-
// Search for memory file
|
|
37
|
-
for (const filename of MEMORY_FILES) {
|
|
38
|
-
const filePath = path.join(cwd, filename);
|
|
39
|
-
if (fs.existsSync(filePath)) {
|
|
40
|
-
try {
|
|
41
|
-
const stat = fs.statSync(filePath);
|
|
42
|
-
const originalSize = stat.size;
|
|
43
|
-
// Read file content
|
|
44
|
-
let content = fs.readFileSync(filePath, 'utf-8');
|
|
45
|
-
let truncated = false;
|
|
46
|
-
// Truncate if too large
|
|
47
|
-
if (content.length > maxSize) {
|
|
48
|
-
content = content.slice(0, maxSize);
|
|
49
|
-
// Try to truncate at a line boundary
|
|
50
|
-
const lastNewline = content.lastIndexOf('\n');
|
|
51
|
-
if (lastNewline > maxSize * 0.8) {
|
|
52
|
-
content = content.slice(0, lastNewline);
|
|
53
|
-
}
|
|
54
|
-
content += '\n\n[... truncated due to size ...]';
|
|
55
|
-
truncated = true;
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
found: true,
|
|
59
|
-
content,
|
|
60
|
-
filePath,
|
|
61
|
-
originalSize,
|
|
62
|
-
truncated,
|
|
63
|
-
estimatedTokens: estimateTokens(content),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// File exists but couldn't be read, continue to next
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// No memory file found
|
|
73
|
-
return {
|
|
74
|
-
found: false,
|
|
75
|
-
content: '',
|
|
76
|
-
filePath: null,
|
|
77
|
-
originalSize: 0,
|
|
78
|
-
truncated: false,
|
|
79
|
-
estimatedTokens: 0,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Check if a project memory file exists without loading it
|
|
84
|
-
*/
|
|
85
|
-
export function hasProjectMemory(cwd) {
|
|
86
|
-
const dir = cwd ?? process.cwd();
|
|
87
|
-
for (const filename of MEMORY_FILES) {
|
|
88
|
-
const filePath = path.join(dir, filename);
|
|
89
|
-
if (fs.existsSync(filePath)) {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Get the size category for display purposes
|
|
97
|
-
*/
|
|
98
|
-
export function getSizeCategory(bytes) {
|
|
99
|
-
if (bytes < DEFAULT_WARN_SIZE) {
|
|
100
|
-
return 'small';
|
|
101
|
-
}
|
|
102
|
-
else if (bytes < DEFAULT_MAX_SIZE) {
|
|
103
|
-
return 'medium';
|
|
104
|
-
}
|
|
105
|
-
return 'large';
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Format bytes as human-readable string
|
|
8
|
+
* The previous CLI-only display helpers (hasProjectMemory / getSizeCategory /
|
|
9
|
+
* formatBytes) had no callers and were removed.
|
|
109
10
|
*/
|
|
110
|
-
export
|
|
111
|
-
if (bytes < 1024) {
|
|
112
|
-
return `${String(bytes)} B`;
|
|
113
|
-
}
|
|
114
|
-
else if (bytes < 1024 * 1024) {
|
|
115
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
116
|
-
}
|
|
117
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
118
|
-
}
|
|
11
|
+
export { loadProjectContextFiles as loadProjectMemory } from '@compilr-dev/sdk';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compilr-dev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "AI-powered coding assistant CLI using @compilr-dev/agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
58
|
-
"@compilr-dev/agents": "^0.
|
|
58
|
+
"@compilr-dev/agents": "^0.6.0",
|
|
59
59
|
"@compilr-dev/agents-coding": "^1.0.4",
|
|
60
60
|
"@compilr-dev/editor-core": "^0.0.2",
|
|
61
|
-
"@compilr-dev/factory": "^0.1.
|
|
61
|
+
"@compilr-dev/factory": "^0.1.35",
|
|
62
62
|
"@compilr-dev/logger": "^0.1.0",
|
|
63
|
-
"@compilr-dev/sdk": "^0.
|
|
63
|
+
"@compilr-dev/sdk": "^0.14.0",
|
|
64
64
|
"@compilr-dev/ui-core": "^0.0.1",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
66
66
|
"ansi-escapes": "^7.3.0",
|