archbyte 0.1.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/README.md +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- package/ui/dist/index.html +14 -0
package/dist/cli/auth.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as http from "http";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".archbyte");
|
|
7
|
+
const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
8
|
+
const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
|
|
9
|
+
const CLI_CALLBACK_PORT = 19274;
|
|
10
|
+
export async function handleLogin(provider) {
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(chalk.bold.cyan("ArchByte Login"));
|
|
13
|
+
console.log();
|
|
14
|
+
const existing = loadCredentials();
|
|
15
|
+
if (existing && !isExpired(existing)) {
|
|
16
|
+
// Refresh tier from server in case it changed (e.g. upgraded via dashboard)
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`${API_BASE}/api/v1/me`, {
|
|
19
|
+
headers: { Authorization: `Bearer ${existing.token}` },
|
|
20
|
+
});
|
|
21
|
+
if (res.ok) {
|
|
22
|
+
const { user } = (await res.json());
|
|
23
|
+
if (user.tier !== existing.tier) {
|
|
24
|
+
existing.tier = user.tier;
|
|
25
|
+
saveCredentials(existing);
|
|
26
|
+
}
|
|
27
|
+
// Cache server-verified tier
|
|
28
|
+
cacheVerifiedTier(user.tier === "premium" ? "premium" : "free", existing.email);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Offline — use cached tier
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk.green(`Already logged in as ${chalk.bold(existing.email)} (${existing.tier} tier)`));
|
|
35
|
+
console.log(chalk.gray("Run `archbyte logout` to sign out."));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const selectedProvider = provider ?? "github";
|
|
39
|
+
console.log(chalk.gray(`Opening browser for ${selectedProvider} authentication...`));
|
|
40
|
+
console.log();
|
|
41
|
+
try {
|
|
42
|
+
const credentials = await startOAuthFlow(selectedProvider);
|
|
43
|
+
saveCredentials(credentials);
|
|
44
|
+
// Cache server-verified tier from fresh login
|
|
45
|
+
cacheVerifiedTier(credentials.tier === "premium" ? "premium" : "free", credentials.email);
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.green(`Logged in as ${chalk.bold(credentials.email)} (${credentials.tier} tier)`));
|
|
48
|
+
console.log(chalk.gray(`Credentials saved to ${CREDENTIALS_PATH}`));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(chalk.gray("You can also log in manually:"));
|
|
54
|
+
console.log(chalk.gray(` 1. Visit ${API_BASE.replace("api.", "")}/auth`));
|
|
55
|
+
console.log(chalk.gray(" 2. Copy your token"));
|
|
56
|
+
console.log(chalk.gray(" 3. Run: archbyte login --token <your-token>"));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function handleLoginWithToken(token) {
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(chalk.bold.cyan("ArchByte Login (token)"));
|
|
63
|
+
console.log();
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${API_BASE}/api/v1/me`, {
|
|
66
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
console.error(chalk.red("Invalid token."));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const { user } = (await res.json());
|
|
73
|
+
const payload = parseJWTPayload(token);
|
|
74
|
+
saveCredentials({
|
|
75
|
+
token,
|
|
76
|
+
email: user.email,
|
|
77
|
+
tier: user.tier,
|
|
78
|
+
expiresAt: payload?.exp
|
|
79
|
+
? new Date(payload.exp * 1000).toISOString()
|
|
80
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
81
|
+
});
|
|
82
|
+
// Cache server-verified tier
|
|
83
|
+
cacheVerifiedTier(user.tier === "premium" ? "premium" : "free", user.email);
|
|
84
|
+
console.log(chalk.green(`Logged in as ${chalk.bold(user.email)} (${user.tier} tier)`));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// === Logout ===
|
|
92
|
+
export async function handleLogout() {
|
|
93
|
+
console.log();
|
|
94
|
+
const creds = loadCredentials();
|
|
95
|
+
if (!creds) {
|
|
96
|
+
console.log(chalk.gray("Not logged in."));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
fs.unlinkSync(CREDENTIALS_PATH);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Ignore
|
|
104
|
+
}
|
|
105
|
+
// Clean up tier cache and offline action tracker
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(TIER_CACHE_PATH);
|
|
108
|
+
}
|
|
109
|
+
catch { /* Ignore */ }
|
|
110
|
+
resetOfflineActions();
|
|
111
|
+
console.log(chalk.green(`Logged out (was ${creds.email}).`));
|
|
112
|
+
}
|
|
113
|
+
// === Status ===
|
|
114
|
+
export async function handleStatus() {
|
|
115
|
+
console.log();
|
|
116
|
+
console.log(chalk.bold.cyan("ArchByte Account"));
|
|
117
|
+
console.log();
|
|
118
|
+
const creds = loadCredentials();
|
|
119
|
+
if (!creds) {
|
|
120
|
+
console.log(chalk.yellow("Not logged in. Run `archbyte login` to sign in."));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (isExpired(creds)) {
|
|
124
|
+
console.log(chalk.yellow("Session expired. Run `archbyte login` to refresh."));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
console.log(` ${chalk.bold("Email")}: ${creds.email}`);
|
|
128
|
+
console.log(` ${chalk.bold("Tier")}: ${creds.tier === "premium" ? chalk.green("Pro") : "Basic"}`);
|
|
129
|
+
console.log(` ${chalk.bold("Expires")}: ${new Date(creds.expiresAt).toLocaleDateString()}`);
|
|
130
|
+
// Fetch live usage from server
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`${API_BASE}/api/v1/scans/count`, {
|
|
133
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
134
|
+
});
|
|
135
|
+
if (res.ok) {
|
|
136
|
+
const data = (await res.json());
|
|
137
|
+
console.log(` ${chalk.bold("Scans")}: ${data.scansUsed}${data.scansAllowed === -1 ? " (unlimited)" : `/${data.scansAllowed}`}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Offline — show cached info only
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
// === Credential Helpers (exported for license gate) ===
|
|
146
|
+
export function loadCredentials() {
|
|
147
|
+
try {
|
|
148
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
149
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
150
|
+
// Validate structure — reject malformed credentials
|
|
151
|
+
if (typeof data?.token !== "string" ||
|
|
152
|
+
typeof data?.email !== "string" ||
|
|
153
|
+
typeof data?.tier !== "string" ||
|
|
154
|
+
typeof data?.expiresAt !== "string") {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Ignore — corrupt file treated as no credentials
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function saveCredentials(creds) {
|
|
166
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
167
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), "utf-8");
|
|
170
|
+
// Restrict permissions (owner-only read/write)
|
|
171
|
+
try {
|
|
172
|
+
fs.chmodSync(CREDENTIALS_PATH, 0o600);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Windows doesn't support chmod
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function isExpired(creds) {
|
|
179
|
+
return new Date(creds.expiresAt) < new Date();
|
|
180
|
+
}
|
|
181
|
+
function parseJWTPayload(token) {
|
|
182
|
+
try {
|
|
183
|
+
const parts = token.split(".");
|
|
184
|
+
if (parts.length !== 3)
|
|
185
|
+
return null;
|
|
186
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get the tier from the JWT token payload. NEVER falls back to the
|
|
194
|
+
* editable credentials file — that would allow trivial tier bypass
|
|
195
|
+
* by editing ~/.archbyte/credentials.json.
|
|
196
|
+
*
|
|
197
|
+
* WARNING: This reads from an UNVERIFIED JWT payload. The CLI cannot
|
|
198
|
+
* verify HMAC signatures (no shared secret). Use getVerifiedTier()
|
|
199
|
+
* instead, which reads from the server-verified tier cache.
|
|
200
|
+
*/
|
|
201
|
+
export function getTierFromToken() {
|
|
202
|
+
const creds = loadCredentials();
|
|
203
|
+
if (!creds?.token)
|
|
204
|
+
return "free";
|
|
205
|
+
const payload = parseJWTPayload(creds.token);
|
|
206
|
+
return payload?.tier === "premium" ? "premium" : "free";
|
|
207
|
+
}
|
|
208
|
+
// === Server-Verified Tier Cache ===
|
|
209
|
+
// The CLI cannot verify JWT signatures (HMAC requires the server secret).
|
|
210
|
+
// Instead, we cache the tier returned by the server after successful
|
|
211
|
+
// verification. This cache is the ONLY trusted source of tier info locally.
|
|
212
|
+
const TIER_CACHE_PATH = path.join(CONFIG_DIR, "tier-cache.json");
|
|
213
|
+
const TIER_CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
214
|
+
/**
|
|
215
|
+
* Cache the server-verified tier. Called by license-gate after a
|
|
216
|
+
* successful server response. Uses email as the identifier since
|
|
217
|
+
* the license-gate has access to it from credentials.
|
|
218
|
+
*/
|
|
219
|
+
export function cacheVerifiedTier(tier, email) {
|
|
220
|
+
const data = { tier, verifiedAt: Date.now(), email };
|
|
221
|
+
try {
|
|
222
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
223
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
fs.writeFileSync(TIER_CACHE_PATH, JSON.stringify(data), "utf-8");
|
|
226
|
+
try {
|
|
227
|
+
fs.chmodSync(TIER_CACHE_PATH, 0o600);
|
|
228
|
+
}
|
|
229
|
+
catch { /* Windows */ }
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Non-critical — offline fallback just won't work
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get the tier from the server-verified cache. Returns "free" if cache
|
|
237
|
+
* is missing, stale (> 1 hour), or belongs to a different user.
|
|
238
|
+
*/
|
|
239
|
+
export function getVerifiedTier() {
|
|
240
|
+
try {
|
|
241
|
+
if (!fs.existsSync(TIER_CACHE_PATH))
|
|
242
|
+
return "free";
|
|
243
|
+
const data = JSON.parse(fs.readFileSync(TIER_CACHE_PATH, "utf-8"));
|
|
244
|
+
// Validate cache structure
|
|
245
|
+
if (!data || typeof data.verifiedAt !== "number" || typeof data.tier !== "string") {
|
|
246
|
+
return "free";
|
|
247
|
+
}
|
|
248
|
+
// Check staleness
|
|
249
|
+
if (Date.now() - data.verifiedAt > TIER_CACHE_MAX_AGE_MS)
|
|
250
|
+
return "free";
|
|
251
|
+
// Check user match (prevent tier cache from a different account)
|
|
252
|
+
const creds = loadCredentials();
|
|
253
|
+
if (creds?.email && data.email !== creds.email)
|
|
254
|
+
return "free";
|
|
255
|
+
return data.tier === "premium" ? "premium" : "free";
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return "free";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// === Offline Action Tracking ===
|
|
262
|
+
const OFFLINE_ACTIONS_PATH = path.join(CONFIG_DIR, "offline-actions.json");
|
|
263
|
+
const OFFLINE_MAX_PREMIUM = 3; // Premium users: 3 offline actions within cache window
|
|
264
|
+
const OFFLINE_MAX_FREE = 0; // Free users: 0 offline actions (must verify online)
|
|
265
|
+
/**
|
|
266
|
+
* Check if an offline action is allowed. Returns true if within limits.
|
|
267
|
+
* Increments the counter when allowed.
|
|
268
|
+
*/
|
|
269
|
+
export function checkOfflineAction() {
|
|
270
|
+
const tier = getVerifiedTier();
|
|
271
|
+
const maxActions = tier === "premium" ? OFFLINE_MAX_PREMIUM : OFFLINE_MAX_FREE;
|
|
272
|
+
if (maxActions === 0) {
|
|
273
|
+
return {
|
|
274
|
+
allowed: false,
|
|
275
|
+
reason: "Server verification required. Check your internet connection and try again.",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
let data = { count: 0, lastReset: Date.now() };
|
|
280
|
+
if (fs.existsSync(OFFLINE_ACTIONS_PATH)) {
|
|
281
|
+
data = JSON.parse(fs.readFileSync(OFFLINE_ACTIONS_PATH, "utf-8"));
|
|
282
|
+
}
|
|
283
|
+
// Reset counter if cache window expired
|
|
284
|
+
if (Date.now() - data.lastReset > TIER_CACHE_MAX_AGE_MS) {
|
|
285
|
+
data = { count: 0, lastReset: Date.now() };
|
|
286
|
+
}
|
|
287
|
+
if (data.count >= maxActions) {
|
|
288
|
+
return {
|
|
289
|
+
allowed: false,
|
|
290
|
+
reason: `Offline action limit reached (${maxActions}/${maxActions}). Reconnect to the license server to continue.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// Increment and save
|
|
294
|
+
data.count++;
|
|
295
|
+
fs.writeFileSync(OFFLINE_ACTIONS_PATH, JSON.stringify(data), "utf-8");
|
|
296
|
+
try {
|
|
297
|
+
fs.chmodSync(OFFLINE_ACTIONS_PATH, 0o600);
|
|
298
|
+
}
|
|
299
|
+
catch { /* Windows */ }
|
|
300
|
+
return { allowed: true };
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return { allowed: false, reason: "Could not verify offline action limit." };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Reset offline action counter. Called after successful server verification.
|
|
308
|
+
*/
|
|
309
|
+
export function resetOfflineActions() {
|
|
310
|
+
try {
|
|
311
|
+
if (fs.existsSync(OFFLINE_ACTIONS_PATH)) {
|
|
312
|
+
fs.unlinkSync(OFFLINE_ACTIONS_PATH);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Non-critical
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// === OAuth Flow ===
|
|
320
|
+
function startOAuthFlow(provider = "github") {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const timeout = setTimeout(() => {
|
|
323
|
+
server.close();
|
|
324
|
+
reject(new Error("Login timed out (60s). Try again or use --token."));
|
|
325
|
+
}, 60000);
|
|
326
|
+
const server = http.createServer(async (req, res) => {
|
|
327
|
+
const url = new URL(req.url ?? "/", `http://localhost:${CLI_CALLBACK_PORT}`);
|
|
328
|
+
if (url.pathname === "/callback") {
|
|
329
|
+
const token = url.searchParams.get("token");
|
|
330
|
+
if (!token) {
|
|
331
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
332
|
+
res.end("<h1>Login failed</h1><p>No token received. Close this window and try again.</p>");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
336
|
+
res.end("<h1>Login successful!</h1><p>You can close this window and return to your terminal.</p>");
|
|
337
|
+
clearTimeout(timeout);
|
|
338
|
+
server.close();
|
|
339
|
+
// Fetch user info with the token
|
|
340
|
+
try {
|
|
341
|
+
const meRes = await fetch(`${API_BASE}/api/v1/me`, {
|
|
342
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
343
|
+
});
|
|
344
|
+
if (!meRes.ok) {
|
|
345
|
+
reject(new Error("Failed to fetch user info"));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const { user } = (await meRes.json());
|
|
349
|
+
const payload = parseJWTPayload(token);
|
|
350
|
+
resolve({
|
|
351
|
+
token,
|
|
352
|
+
email: user.email,
|
|
353
|
+
tier: user.tier,
|
|
354
|
+
expiresAt: payload?.exp
|
|
355
|
+
? new Date(payload.exp * 1000).toISOString()
|
|
356
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
reject(err);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
server.listen(CLI_CALLBACK_PORT, "127.0.0.1", () => {
|
|
365
|
+
const authUrl = `${API_BASE}/api/v1/auth/${provider}?cli=1`;
|
|
366
|
+
// Open browser without shell interpolation (prevents command injection)
|
|
367
|
+
const opener = process.platform === "darwin"
|
|
368
|
+
? "open"
|
|
369
|
+
: process.platform === "win32"
|
|
370
|
+
? "start"
|
|
371
|
+
: "xdg-open";
|
|
372
|
+
const child = spawn(opener, [authUrl], { stdio: "ignore" });
|
|
373
|
+
child.on("error", () => {
|
|
374
|
+
console.log(chalk.yellow("Could not open browser automatically."));
|
|
375
|
+
console.log(chalk.gray(`Open this URL manually: ${authUrl}`));
|
|
376
|
+
});
|
|
377
|
+
child.on("close", (code) => {
|
|
378
|
+
if (code === 0) {
|
|
379
|
+
console.log(chalk.gray("Browser opened. Waiting for authentication..."));
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
console.log(chalk.yellow("Could not open browser automatically."));
|
|
383
|
+
console.log(chalk.gray(`Open this URL manually: ${authUrl}`));
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
server.on("error", (err) => {
|
|
388
|
+
clearTimeout(timeout);
|
|
389
|
+
if (err.code === "EADDRINUSE") {
|
|
390
|
+
reject(new Error(`Port ${CLI_CALLBACK_PORT} is in use. Close other archbyte instances and try again.`));
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
reject(err);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArchByteConfig } from "../agents/runtime/types.js";
|
|
2
|
+
interface ConfigOptions {
|
|
3
|
+
args: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function handleConfig(options: ConfigOptions): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the full ArchByteConfig from config file + env vars.
|
|
8
|
+
* Env vars override config file.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveConfig(): ArchByteConfig | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".archbyte");
|
|
5
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
const VALID_PROVIDERS = ["anthropic", "openai", "google", "ollama"];
|
|
7
|
+
export async function handleConfig(options) {
|
|
8
|
+
const [action, key, value] = options.args;
|
|
9
|
+
if (!action || action === "show") {
|
|
10
|
+
showConfig();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (action === "set") {
|
|
14
|
+
if (!key || !value) {
|
|
15
|
+
console.error(chalk.red("Usage: archbyte config set <key> <value>"));
|
|
16
|
+
console.error(chalk.gray(" Keys: provider, api-key, ollama-url"));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
setConfig(key, value);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (action === "get") {
|
|
23
|
+
if (!key) {
|
|
24
|
+
console.error(chalk.red("Usage: archbyte config get <key>"));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
getConfig(key);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (action === "path") {
|
|
31
|
+
console.log(CONFIG_PATH);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.error(chalk.red(`Unknown action: ${action}`));
|
|
35
|
+
console.error(chalk.gray(" archbyte config show — show current config"));
|
|
36
|
+
console.error(chalk.gray(" archbyte config set <k> <v> — set a config value"));
|
|
37
|
+
console.error(chalk.gray(" archbyte config get <k> — get a config value"));
|
|
38
|
+
console.error(chalk.gray(" archbyte config path — show config file path"));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
function loadConfig() {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
44
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
function saveConfig(config) {
|
|
53
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
54
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
57
|
+
}
|
|
58
|
+
function showConfig() {
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(chalk.bold.cyan("ArchByte Configuration"));
|
|
62
|
+
console.log(chalk.gray(`Config file: ${CONFIG_PATH}`));
|
|
63
|
+
console.log();
|
|
64
|
+
if (!config.provider && !config.apiKey) {
|
|
65
|
+
console.log(chalk.yellow("No configuration found. Set up with:"));
|
|
66
|
+
console.log(chalk.gray(" archbyte config set provider anthropic"));
|
|
67
|
+
console.log(chalk.gray(" archbyte config set api-key sk-ant-..."));
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(chalk.gray("Or use environment variables:"));
|
|
70
|
+
console.log(chalk.gray(" export ARCHBYTE_PROVIDER=anthropic"));
|
|
71
|
+
console.log(chalk.gray(" export ARCHBYTE_API_KEY=sk-ant-..."));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(` ${chalk.bold("provider")}: ${config.provider ?? chalk.gray("not set")}`);
|
|
75
|
+
console.log(` ${chalk.bold("api-key")}: ${config.apiKey ? maskKey(config.apiKey) : chalk.gray("not set")}`);
|
|
76
|
+
if (config.ollamaBaseUrl) {
|
|
77
|
+
console.log(` ${chalk.bold("ollama-url")}: ${config.ollamaBaseUrl}`);
|
|
78
|
+
}
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
function setConfig(key, value) {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
switch (key) {
|
|
84
|
+
case "provider": {
|
|
85
|
+
if (!VALID_PROVIDERS.includes(value)) {
|
|
86
|
+
console.error(chalk.red(`Invalid provider: ${value}. Must be: ${VALID_PROVIDERS.join(", ")}`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
config.provider = value;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "api-key":
|
|
93
|
+
case "apiKey":
|
|
94
|
+
case "key":
|
|
95
|
+
config.apiKey = value;
|
|
96
|
+
break;
|
|
97
|
+
case "ollama-url":
|
|
98
|
+
case "ollamaUrl":
|
|
99
|
+
config.ollamaBaseUrl = value;
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
console.error(chalk.red(`Unknown config key: ${key}`));
|
|
103
|
+
console.error(chalk.gray(" Valid keys: provider, api-key, ollama-url"));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
saveConfig(config);
|
|
107
|
+
console.log(chalk.green(`Set ${key} = ${key.includes("key") ? maskKey(value) : value}`));
|
|
108
|
+
}
|
|
109
|
+
function getConfig(key) {
|
|
110
|
+
const config = loadConfig();
|
|
111
|
+
switch (key) {
|
|
112
|
+
case "provider":
|
|
113
|
+
console.log(config.provider ?? "");
|
|
114
|
+
break;
|
|
115
|
+
case "api-key":
|
|
116
|
+
case "apiKey":
|
|
117
|
+
case "key":
|
|
118
|
+
console.log(config.apiKey ?? "");
|
|
119
|
+
break;
|
|
120
|
+
case "ollama-url":
|
|
121
|
+
case "ollamaUrl":
|
|
122
|
+
console.log(config.ollamaBaseUrl ?? "");
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
console.error(chalk.red(`Unknown config key: ${key}`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function maskKey(key) {
|
|
130
|
+
if (key.length <= 8)
|
|
131
|
+
return "****";
|
|
132
|
+
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve the full ArchByteConfig from config file + env vars.
|
|
136
|
+
* Env vars override config file.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveConfig() {
|
|
139
|
+
const config = loadConfig();
|
|
140
|
+
const provider = process.env.ARCHBYTE_PROVIDER ?? config.provider;
|
|
141
|
+
const apiKey = process.env.ARCHBYTE_API_KEY ?? config.apiKey;
|
|
142
|
+
// Auto-detect from known env vars if nothing explicit
|
|
143
|
+
if (!provider && !apiKey) {
|
|
144
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
145
|
+
return { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY };
|
|
146
|
+
}
|
|
147
|
+
if (process.env.OPENAI_API_KEY) {
|
|
148
|
+
return { provider: "openai", apiKey: process.env.OPENAI_API_KEY };
|
|
149
|
+
}
|
|
150
|
+
if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
|
|
151
|
+
return {
|
|
152
|
+
provider: "google",
|
|
153
|
+
apiKey: process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (!provider)
|
|
159
|
+
return null;
|
|
160
|
+
// Ollama doesn't need an API key
|
|
161
|
+
if (provider === "ollama") {
|
|
162
|
+
return {
|
|
163
|
+
provider: "ollama",
|
|
164
|
+
apiKey: "",
|
|
165
|
+
ollamaBaseUrl: process.env.OLLAMA_BASE_URL ??
|
|
166
|
+
config.ollamaBaseUrl ??
|
|
167
|
+
"http://localhost:11434",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!apiKey)
|
|
171
|
+
return null;
|
|
172
|
+
return {
|
|
173
|
+
provider,
|
|
174
|
+
apiKey,
|
|
175
|
+
ollamaBaseUrl: config.ollamaBaseUrl,
|
|
176
|
+
};
|
|
177
|
+
}
|