archbyte 0.3.4 → 0.4.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 CHANGED
@@ -76,6 +76,48 @@ Run `/archbyte-help` in Claude Code to see all commands.
76
76
 
77
77
  ## CLI Commands
78
78
 
79
+ ### `archbyte login`
80
+
81
+ Sign in or create a free account. Shows an interactive provider picker (GitHub, Google, Email & Password).
82
+
83
+ ```bash
84
+ archbyte login # interactive provider picker
85
+ archbyte login --github # sign in with GitHub
86
+ archbyte login --google # sign in with Google
87
+ archbyte login --email # sign in with email and password
88
+ archbyte login --token JWT # login with a pre-existing JWT token
89
+ ```
90
+
91
+ Multiple accounts are supported. If already logged in, you'll be prompted to add a different account.
92
+
93
+ ### `archbyte logout`
94
+
95
+ Sign out of ArchByte.
96
+
97
+ ```bash
98
+ archbyte logout # logout active account
99
+ archbyte logout user@co.com # logout specific account
100
+ archbyte logout --all # logout all accounts
101
+ ```
102
+
103
+ ### `archbyte accounts`
104
+
105
+ List and manage logged-in accounts.
106
+
107
+ ```bash
108
+ archbyte accounts # list all accounts
109
+ archbyte accounts switch # interactive account switcher
110
+ archbyte accounts switch user@co.com # switch to specific account
111
+ ```
112
+
113
+ ### `archbyte status`
114
+
115
+ Show account status, tier, and usage.
116
+
117
+ ```bash
118
+ archbyte status
119
+ ```
120
+
79
121
  ### `archbyte init`
80
122
 
81
123
  Scaffold an `archbyte.yaml` config and `.archbyte/` directory.
package/bin/archbyte.js CHANGED
@@ -18,7 +18,7 @@ import { handlePatrol } from '../dist/cli/patrol.js';
18
18
  import { handleWorkflow } from '../dist/cli/workflow.js';
19
19
  import { handleAnalyze } from '../dist/cli/analyze.js';
20
20
  import { handleConfig } from '../dist/cli/config.js';
21
- import { handleLogin, handleLoginWithToken, handleLogout, handleStatus } from '../dist/cli/auth.js';
21
+ import { handleLogin, handleLoginWithToken, handleLogout, handleStatus, handleAccounts, handleAccountSwitch } from '../dist/cli/auth.js';
22
22
  import { handleRun } from '../dist/cli/run.js';
23
23
  import { handleSetup } from '../dist/cli/setup.js';
24
24
  import { handleVersion, handleUpdate } from '../dist/cli/version.js';
@@ -32,17 +32,15 @@ const program = new Command();
32
32
 
33
33
  program
34
34
  .name('archbyte')
35
- .description('ArchByte - See what agents build. As they build it.')
36
- .version(PKG_VERSION)
35
+ .description('ArchByte - AI architecture analysis with an interactive diagram UI')
36
+ .version(PKG_VERSION, '-v, --version', 'Show version number')
37
37
  .addHelpText('after', `
38
38
  Quick start:
39
- 1. Sign up at https://archbyte.heartbyte.io
40
- 2. $ archbyte login Sign in from the CLI
41
- 3. $ archbyte init Configure your model provider
42
- 4. $ archbyte run Analyze → generate → serve
39
+ $ archbyte login Sign in or create a free account
40
+ $ archbyte init Configure your model provider
41
+ $ archbyte run Analyze + open interactive diagram UI
43
42
 
44
43
  https://archbyte.heartbyte.io
45
- Support: archbyte@heartbyte.io
46
44
  `);
47
45
 
48
46
  // — Getting started —
@@ -53,11 +51,13 @@ program
53
51
  .option('--token <jwt>', 'Login with a pre-existing JWT token')
54
52
  .option('--github', 'Sign in with GitHub')
55
53
  .option('--google', 'Sign in with Google')
54
+ .option('--email', 'Sign in with email and password')
56
55
  .action(async (options) => {
57
56
  if (options.token) {
58
57
  await handleLoginWithToken(options.token);
59
58
  } else {
60
- const provider = options.google ? 'google' : 'github';
59
+ // Explicit provider flag, or null for interactive picker
60
+ const provider = options.github ? 'github' : options.google ? 'google' : options.email ? 'email' : undefined;
61
61
  await handleLogin(provider);
62
62
  }
63
63
  });
@@ -65,8 +65,10 @@ program
65
65
  program
66
66
  .command('logout')
67
67
  .description('Sign out of ArchByte')
68
- .action(async () => {
69
- await handleLogout();
68
+ .argument('[email]', 'Logout a specific account by email')
69
+ .option('--all', 'Logout all accounts')
70
+ .action(async (email, options) => {
71
+ await handleLogout({ email, all: options.all });
70
72
  });
71
73
 
72
74
  program
@@ -169,7 +171,7 @@ program
169
171
  .command('export')
170
172
  .description('Export architecture to various formats')
171
173
  .option('-d, --diagram <path>', 'Path to architecture JSON (default: .archbyte/architecture.json)')
172
- .option('-f, --format <format>', 'Output format: mermaid, markdown, json, plantuml, dot (default: mermaid)')
174
+ .option('-f, --format <format>', 'Output format: mermaid, markdown, json, plantuml, dot, html [Pro] (default: mermaid)')
173
175
  .option('-o, --output <path>', 'Write to file instead of stdout')
174
176
  .action(async (options) => {
175
177
  await handleExport(options);
@@ -214,14 +216,30 @@ program
214
216
  await handleStatus();
215
217
  });
216
218
 
219
+ const accountsCmd = program
220
+ .command('accounts')
221
+ .description('List and manage logged-in accounts')
222
+ .action(async () => {
223
+ await handleAccounts();
224
+ });
225
+
226
+ accountsCmd
227
+ .command('switch')
228
+ .description('Switch the active account')
229
+ .argument('[email]', 'Switch to a specific account by email')
230
+ .action(async (email) => {
231
+ await handleAccountSwitch(email);
232
+ });
233
+
217
234
  program
218
235
  .command('config')
219
236
  .description('Manage ArchByte configuration (provider, API key)')
220
237
  .argument('[action]', 'show, set, get, or path')
221
238
  .argument('[key]', 'config key (provider, api-key, model)')
222
239
  .argument('[value]', 'config value')
223
- .action(async (action, key, value) => {
224
- await handleConfig({ args: [action, key, value].filter(Boolean) });
240
+ .option('--raw', 'Show unmasked values (for scripting)')
241
+ .action(async (action, key, value, options) => {
242
+ await handleConfig({ args: [action, key, value].filter(Boolean), raw: options.raw });
225
243
  });
226
244
 
227
245
  program
@@ -1,5 +1,10 @@
1
1
  // Pipeline — Merger
2
2
  // Assembles all agent outputs into a StaticAnalysisResult
3
+ function sanitize(s) {
4
+ if (!s)
5
+ return s;
6
+ return s.replace(/\u2014/g, "-").replace(/\u2013/g, "-").replace(/\u2018|\u2019/g, "'").replace(/\u201C|\u201D/g, '"');
7
+ }
3
8
  /**
4
9
  * Merge all pipeline agent outputs into a StaticAnalysisResult
5
10
  * compatible with the existing buildAnalysisFromStatic() in cli/analyze.ts.
@@ -11,11 +16,11 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
11
16
  for (const c of componentId.components) {
12
17
  components.push({
13
18
  id: c.id,
14
- name: c.name,
19
+ name: sanitize(c.name) ?? c.name,
15
20
  type: c.type,
16
21
  layer: c.layer,
17
22
  path: c.path,
18
- description: c.description,
23
+ description: sanitize(c.description),
19
24
  technologies: c.technologies,
20
25
  });
21
26
  }
@@ -46,7 +51,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
46
51
  type: "database",
47
52
  layer: "data",
48
53
  path: "",
49
- description: db.description,
54
+ description: sanitize(db.description),
50
55
  technologies: [db.type],
51
56
  });
52
57
  componentIds.add(db.id);
@@ -59,11 +64,11 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
59
64
  if (!componentIds.has(svc.id)) {
60
65
  components.push({
61
66
  id: svc.id,
62
- name: svc.name,
67
+ name: sanitize(svc.name) ?? svc.name,
63
68
  type: "service",
64
69
  layer: "external",
65
70
  path: "",
66
- description: svc.description,
71
+ description: sanitize(svc.description),
67
72
  technologies: [svc.type],
68
73
  });
69
74
  componentIds.add(svc.id);
@@ -96,7 +101,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
96
101
  // Apply validator description improvements
97
102
  if (validatorOut?.componentDescriptions) {
98
103
  for (const comp of components) {
99
- const better = validatorOut.componentDescriptions[comp.id];
104
+ const better = sanitize(validatorOut.componentDescriptions[comp.id]);
100
105
  if (better && better.length > (comp.description?.length ?? 0)) {
101
106
  comp.description = better;
102
107
  }
@@ -118,7 +123,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
118
123
  from: c.from,
119
124
  to: c.to,
120
125
  type: c.type,
121
- description: c.description,
126
+ description: sanitize(c.description),
122
127
  confidence: 80,
123
128
  async: c.async,
124
129
  });
@@ -198,7 +203,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
198
203
  from: c.from,
199
204
  to: c.to,
200
205
  type: c.type,
201
- description: c.description,
206
+ description: sanitize(c.description),
202
207
  confidence: 65,
203
208
  async: c.async,
204
209
  });
@@ -247,7 +252,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
247
252
  // Override docs from service-describer
248
253
  const docs = { ...ctx.docs };
249
254
  if (serviceDesc?.projectDescription && serviceDesc.projectDescription.length > (docs.projectDescription?.length ?? 0)) {
250
- docs.projectDescription = serviceDesc.projectDescription;
255
+ docs.projectDescription = sanitize(serviceDesc.projectDescription);
251
256
  }
252
257
  return {
253
258
  structure,
@@ -259,8 +264,8 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
259
264
  connections: {
260
265
  connections: filteredConnections,
261
266
  flows: flows.map((f) => ({
262
- name: f.name,
263
- description: f.description,
267
+ name: sanitize(f.name) ?? f.name,
268
+ description: sanitize(f.description),
264
269
  category: f.category,
265
270
  steps: f.steps,
266
271
  })),
@@ -0,0 +1,7 @@
1
+ import type { LLMProvider, ChatParams, LLMResponse, LLMChunk } from "../runtime/types.js";
2
+ export declare class ClaudeSdkProvider implements LLMProvider {
3
+ name: "claude-sdk";
4
+ chat(params: ChatParams): Promise<LLMResponse>;
5
+ stream(params: ChatParams): AsyncIterable<LLMChunk>;
6
+ private extractPrompt;
7
+ }
@@ -0,0 +1,59 @@
1
+ export class ClaudeSdkProvider {
2
+ name = "claude-sdk";
3
+ async chat(params) {
4
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
5
+ const prompt = this.extractPrompt(params.messages);
6
+ const result = query({
7
+ prompt,
8
+ options: {
9
+ systemPrompt: params.system,
10
+ allowedTools: [],
11
+ maxTurns: 1,
12
+ ...(params.model ? { model: params.model } : {}),
13
+ permissionMode: "bypassPermissions",
14
+ allowDangerouslySkipPermissions: true,
15
+ },
16
+ });
17
+ let resultText = "";
18
+ let usage = { inputTokens: 0, outputTokens: 0 };
19
+ for await (const message of result) {
20
+ if (message.type === "result") {
21
+ if (message.subtype === "success") {
22
+ resultText = message.result;
23
+ }
24
+ usage = {
25
+ inputTokens: message.usage.input_tokens ?? 0,
26
+ outputTokens: message.usage.output_tokens ?? 0,
27
+ };
28
+ }
29
+ }
30
+ return {
31
+ content: [{ type: "text", text: resultText }],
32
+ stopReason: "end_turn",
33
+ usage,
34
+ };
35
+ }
36
+ async *stream(params) {
37
+ // Pipeline agents don't use streaming — delegate to chat()
38
+ const response = await this.chat(params);
39
+ const text = response.content.find((b) => b.type === "text")?.text ?? "";
40
+ if (text) {
41
+ yield { type: "text", text };
42
+ }
43
+ yield { type: "done" };
44
+ }
45
+ extractPrompt(messages) {
46
+ for (let i = messages.length - 1; i >= 0; i--) {
47
+ if (messages[i].role === "user") {
48
+ const content = messages[i].content;
49
+ if (typeof content === "string")
50
+ return content;
51
+ return content
52
+ .filter((b) => b.type === "text")
53
+ .map((b) => b.text ?? "")
54
+ .join("\n");
55
+ }
56
+ }
57
+ return "";
58
+ }
59
+ }
@@ -1,7 +1,12 @@
1
1
  import type { LLMProvider, ArchByteConfig } from "../runtime/types.js";
2
2
  export declare function createProvider(config: ArchByteConfig): LLMProvider;
3
+ /**
4
+ * Check if Claude Code CLI is available on PATH.
5
+ */
6
+ export declare function isClaudeCodeAvailable(): boolean;
3
7
  /**
4
8
  * Auto-detect provider from environment variables.
5
9
  * Checks in order: ARCHBYTE_PROVIDER, then falls back to whichever API key is set.
10
+ * Last resort: Claude Code on PATH → claude-sdk (zero config).
6
11
  */
7
12
  export declare function detectConfig(): ArchByteConfig | null;
@@ -1,6 +1,8 @@
1
+ import { execSync } from "child_process";
1
2
  import { AnthropicProvider } from "./anthropic.js";
2
3
  import { OpenAIProvider } from "./openai.js";
3
4
  import { GoogleProvider } from "./google.js";
5
+ import { ClaudeSdkProvider } from "./claude-sdk.js";
4
6
  export function createProvider(config) {
5
7
  switch (config.provider) {
6
8
  case "anthropic":
@@ -9,13 +11,29 @@ export function createProvider(config) {
9
11
  return new OpenAIProvider(config.apiKey);
10
12
  case "google":
11
13
  return new GoogleProvider(config.apiKey);
14
+ case "claude-sdk":
15
+ return new ClaudeSdkProvider();
12
16
  default:
13
17
  throw new Error(`Unknown provider: ${config.provider}`);
14
18
  }
15
19
  }
20
+ /**
21
+ * Check if Claude Code CLI is available on PATH.
22
+ */
23
+ export function isClaudeCodeAvailable() {
24
+ try {
25
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
26
+ execSync(cmd, { stdio: "pipe" });
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
16
33
  /**
17
34
  * Auto-detect provider from environment variables.
18
35
  * Checks in order: ARCHBYTE_PROVIDER, then falls back to whichever API key is set.
36
+ * Last resort: Claude Code on PATH → claude-sdk (zero config).
19
37
  */
20
38
  export function detectConfig() {
21
39
  const explicit = process.env.ARCHBYTE_PROVIDER;
@@ -23,7 +41,11 @@ export function detectConfig() {
23
41
  if (explicit && apiKey) {
24
42
  return { provider: explicit, apiKey };
25
43
  }
26
- // Auto-detect from known env vars
44
+ // Claude Code on PATH → zero-config, preferred
45
+ if (isClaudeCodeAvailable()) {
46
+ return { provider: "claude-sdk" };
47
+ }
48
+ // Fall back to API key env vars
27
49
  if (process.env.ANTHROPIC_API_KEY) {
28
50
  return { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY };
29
51
  }
@@ -97,14 +97,14 @@ export interface License {
97
97
  expiresAt: string;
98
98
  isValid: boolean;
99
99
  }
100
- export type ProviderName = "anthropic" | "openai" | "google";
100
+ export type ProviderName = "anthropic" | "openai" | "google" | "claude-sdk";
101
101
  export interface ProviderProfile {
102
102
  apiKey?: string;
103
103
  model?: string;
104
104
  }
105
105
  export interface ArchByteConfig {
106
106
  provider: ProviderName;
107
- apiKey: string;
107
+ apiKey?: string;
108
108
  model?: string;
109
109
  modelOverrides?: Partial<Record<ModelTier, string>>;
110
110
  profiles?: Record<string, ProviderProfile>;
@@ -6,6 +6,11 @@ export const MODEL_MAP = {
6
6
  standard: "claude-sonnet-4-5-20250929",
7
7
  advanced: "claude-opus-4-6",
8
8
  },
9
+ "claude-sdk": {
10
+ fast: "haiku",
11
+ standard: "sonnet",
12
+ advanced: "opus",
13
+ },
9
14
  openai: {
10
15
  fast: "gpt-5.2",
11
16
  standard: "gpt-5.2",
@@ -72,7 +72,7 @@ export async function handleAnalyze(options) {
72
72
  if (options.provider) {
73
73
  config = {
74
74
  provider: options.provider,
75
- apiKey: options.apiKey ?? config?.apiKey ?? "",
75
+ ...(options.provider !== "claude-sdk" ? { apiKey: options.apiKey ?? config?.apiKey ?? "" } : {}),
76
76
  };
77
77
  }
78
78
  if (options.apiKey && config) {
@@ -81,7 +81,10 @@ export async function handleAnalyze(options) {
81
81
  if (!config) {
82
82
  console.error(chalk.red("No model provider configured."));
83
83
  console.error();
84
- console.error(chalk.bold("Set up with:"));
84
+ console.error(chalk.bold("Zero-config (Claude Code users):"));
85
+ console.error(chalk.gray(" Install Claude Code → archbyte analyze just works"));
86
+ console.error();
87
+ console.error(chalk.bold("Or set up with:"));
85
88
  console.error(chalk.gray(" archbyte config set provider anthropic"));
86
89
  console.error(chalk.gray(" archbyte config set api-key sk-ant-..."));
87
90
  console.error();
@@ -93,10 +96,11 @@ export async function handleAnalyze(options) {
93
96
  console.error(chalk.gray(" archbyte analyze --static"));
94
97
  console.error();
95
98
  console.error(chalk.bold("Supported providers:"));
96
- console.error(chalk.gray(" anthropic, openai, google"));
99
+ console.error(chalk.gray(" anthropic, openai, google, claude-sdk"));
97
100
  process.exit(1);
98
101
  }
99
- console.log(chalk.gray(`Provider: ${chalk.white(config.provider)}`));
102
+ const providerLabel = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
103
+ console.log(chalk.gray(`Provider: ${chalk.white(providerLabel)}`));
100
104
  console.log(chalk.gray(`Project: ${chalk.white(path.basename(rootDir))}`));
101
105
  console.log();
102
106
  // 2. Create provider
@@ -5,10 +5,16 @@ interface Credentials {
5
5
  expiresAt: string;
6
6
  }
7
7
  export type OAuthProvider = "github" | "google";
8
- export declare function handleLogin(provider?: OAuthProvider): Promise<void>;
8
+ export type LoginProvider = OAuthProvider | "email";
9
+ export declare function handleLogin(provider?: LoginProvider): Promise<void>;
9
10
  export declare function handleLoginWithToken(token: string): Promise<void>;
10
- export declare function handleLogout(): Promise<void>;
11
+ export declare function handleLogout(options?: {
12
+ all?: boolean;
13
+ email?: string;
14
+ }): Promise<void>;
11
15
  export declare function handleStatus(): Promise<void>;
16
+ export declare function handleAccounts(): Promise<void>;
17
+ export declare function handleAccountSwitch(email?: string): Promise<void>;
12
18
  export declare function loadCredentials(): Credentials | null;
13
19
  /**
14
20
  * Get the tier from the JWT token payload. NEVER falls back to the
@@ -34,6 +40,9 @@ export declare function getVerifiedTier(): "free" | "premium";
34
40
  /**
35
41
  * Check if an offline action is allowed. Returns true if within limits.
36
42
  * Increments the counter when allowed.
43
+ *
44
+ * Uses atomic write-to-temp-then-rename to prevent race conditions
45
+ * between concurrent CLI invocations.
37
46
  */
38
47
  export declare function checkOfflineAction(): {
39
48
  allowed: boolean;