archbyte 0.3.5 → 0.4.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.
Files changed (45) hide show
  1. package/README.md +42 -0
  2. package/bin/archbyte.js +26 -25
  3. package/dist/agents/pipeline/merger.d.ts +2 -2
  4. package/dist/agents/pipeline/merger.js +165 -35
  5. package/dist/agents/pipeline/types.d.ts +29 -1
  6. package/dist/agents/pipeline/types.js +0 -1
  7. package/dist/agents/providers/claude-sdk.d.ts +7 -0
  8. package/dist/agents/providers/claude-sdk.js +83 -0
  9. package/dist/agents/providers/router.d.ts +5 -0
  10. package/dist/agents/providers/router.js +23 -1
  11. package/dist/agents/runtime/types.d.ts +6 -2
  12. package/dist/agents/runtime/types.js +6 -1
  13. package/dist/agents/static/component-detector.js +35 -3
  14. package/dist/agents/static/connection-mapper.d.ts +1 -1
  15. package/dist/agents/static/connection-mapper.js +74 -1
  16. package/dist/agents/static/index.js +5 -2
  17. package/dist/agents/static/types.d.ts +26 -0
  18. package/dist/cli/analyze.js +65 -18
  19. package/dist/cli/arch-diff.d.ts +38 -0
  20. package/dist/cli/arch-diff.js +61 -0
  21. package/dist/cli/auth.d.ts +8 -2
  22. package/dist/cli/auth.js +241 -31
  23. package/dist/cli/config.js +31 -5
  24. package/dist/cli/export.js +64 -2
  25. package/dist/cli/patrol.d.ts +5 -3
  26. package/dist/cli/patrol.js +417 -65
  27. package/dist/cli/setup.js +76 -8
  28. package/dist/cli/shared.d.ts +11 -0
  29. package/dist/cli/shared.js +61 -0
  30. package/dist/cli/ui.d.ts +9 -0
  31. package/dist/cli/ui.js +59 -5
  32. package/dist/cli/validate.d.ts +0 -1
  33. package/dist/cli/validate.js +0 -16
  34. package/dist/server/src/index.js +593 -19
  35. package/package.json +4 -1
  36. package/templates/archbyte.yaml +8 -0
  37. package/ui/dist/assets/index-DDCNauh7.css +1 -0
  38. package/ui/dist/assets/index-DO4t5Xu1.js +72 -0
  39. package/ui/dist/index.html +2 -2
  40. package/dist/cli/mcp-server.d.ts +0 -1
  41. package/dist/cli/mcp-server.js +0 -443
  42. package/dist/cli/mcp.d.ts +0 -1
  43. package/dist/cli/mcp.js +0 -98
  44. package/ui/dist/assets/index-0_XpUUZQ.css +0 -1
  45. package/ui/dist/assets/index-BTo0zV5E.js +0 -70
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';
@@ -51,11 +51,13 @@ program
51
51
  .option('--token <jwt>', 'Login with a pre-existing JWT token')
52
52
  .option('--github', 'Sign in with GitHub')
53
53
  .option('--google', 'Sign in with Google')
54
+ .option('--email', 'Sign in with email and password')
54
55
  .action(async (options) => {
55
56
  if (options.token) {
56
57
  await handleLoginWithToken(options.token);
57
58
  } else {
58
- 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;
59
61
  await handleLogin(provider);
60
62
  }
61
63
  });
@@ -63,8 +65,10 @@ program
63
65
  program
64
66
  .command('logout')
65
67
  .description('Sign out of ArchByte')
66
- .action(async () => {
67
- 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 });
68
72
  });
69
73
 
70
74
  program
@@ -147,7 +151,6 @@ program
147
151
  .option('-d, --diagram <path>', 'Path to architecture JSON (default: .archbyte/architecture.json)')
148
152
  .option('-c, --config <path>', 'Path to archbyte.yaml config')
149
153
  .option('--ci', 'Machine-readable JSON output for CI pipelines')
150
- .option('-w, --watch', 'Watch for changes and re-validate')
151
154
  .action(async (options) => {
152
155
  await requireLicense('analyze');
153
156
  await handleValidate(options);
@@ -167,7 +170,7 @@ program
167
170
  .command('export')
168
171
  .description('Export architecture to various formats')
169
172
  .option('-d, --diagram <path>', 'Path to architecture JSON (default: .archbyte/architecture.json)')
170
- .option('-f, --format <format>', 'Output format: mermaid, markdown, json, plantuml, dot (default: mermaid)')
173
+ .option('-f, --format <format>', 'Output format: mermaid, markdown, json, plantuml, dot, html [Pro] (default: mermaid)')
171
174
  .option('-o, --output <path>', 'Write to file instead of stdout')
172
175
  .action(async (options) => {
173
176
  await handleExport(options);
@@ -182,7 +185,8 @@ program
182
185
  .option('-c, --config <path>', 'Path to archbyte.yaml config')
183
186
  .option('-i, --interval <duration>', 'Patrol interval: 30s, 5m, 1h (default: 5m)')
184
187
  .option('--on-violation <action>', 'Action on new violations: log, json (default: log)')
185
- .option('--rescan', 'Incremental re-scan when git commit changes')
188
+ .option('--once', 'Run a single patrol cycle then exit')
189
+ .option('-w, --watch', 'Watch source files for changes instead of polling on interval')
186
190
  .option('--history', 'Show patrol history dashboard')
187
191
  .action(async (options) => {
188
192
  await requireLicense('analyze');
@@ -212,6 +216,21 @@ program
212
216
  await handleStatus();
213
217
  });
214
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
+
215
234
  program
216
235
  .command('config')
217
236
  .description('Manage ArchByte configuration (provider, API key)')
@@ -237,24 +256,6 @@ program
237
256
  await handleUpdate();
238
257
  });
239
258
 
240
- // — MCP server —
241
-
242
- const mcpCmd = program
243
- .command('mcp')
244
- .description('Start MCP server for AI coding tools (Claude Code, Codex)')
245
- .action(async () => {
246
- const { startMcpServer } = await import('../dist/cli/mcp-server.js');
247
- await startMcpServer();
248
- });
249
-
250
- mcpCmd
251
- .command('install')
252
- .description('Auto-configure Claude Code and/or Codex CLI')
253
- .action(async () => {
254
- const { handleMcpInstall } = await import('../dist/cli/mcp.js');
255
- await handleMcpInstall();
256
- });
257
-
258
259
  // Default: show help
259
260
  program
260
261
  .action(() => {
@@ -1,7 +1,7 @@
1
1
  import type { StaticAnalysisResult, StaticContext } from "../static/types.js";
2
- import type { ComponentIdentifierOutput, ServiceDescriberOutput, FlowDetectorOutput, ConnectionMapperOutput, ValidatorOutput, IncrementalContext } from "./types.js";
2
+ import type { ComponentIdentifierOutput, ServiceDescriberOutput, FlowDetectorOutput, ConnectionMapperOutput, ValidatorOutput, IncrementalContext, ArchitectureEnricherOutput } from "./types.js";
3
3
  /**
4
4
  * Merge all pipeline agent outputs into a StaticAnalysisResult
5
5
  * compatible with the existing buildAnalysisFromStatic() in cli/analyze.ts.
6
6
  */
7
- export declare function mergeAgentOutputs(ctx: StaticContext, componentId: ComponentIdentifierOutput | null, serviceDesc: ServiceDescriberOutput | null, flowDet: FlowDetectorOutput | null, connMap: ConnectionMapperOutput | null, validatorOut: ValidatorOutput | null, incrementalContext?: IncrementalContext): StaticAnalysisResult;
7
+ export declare function mergeAgentOutputs(ctx: StaticContext, componentId: ComponentIdentifierOutput | null, serviceDesc: ServiceDescriberOutput | null, flowDet: FlowDetectorOutput | null, connMap: ConnectionMapperOutput | null, validatorOut: ValidatorOutput | null, incrementalContext?: IncrementalContext, enricherOut?: ArchitectureEnricherOutput | null): StaticAnalysisResult;
@@ -1,21 +1,112 @@
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
+ }
8
+ /**
9
+ * Build a set of "evidence tokens" from the static context — things that concretely
10
+ * exist in the codebase (dependencies, env vars, docker images/services).
11
+ * Used to gate LLM-generated databases/external services against hallucination.
12
+ */
13
+ function buildEvidenceTokens(ctx) {
14
+ const tokens = new Set();
15
+ // Package dependencies from import map (codeSamples.importMap: file → imported modules)
16
+ for (const imports of Object.values(ctx.codeSamples.importMap)) {
17
+ for (const imp of imports) {
18
+ tokens.add(imp.toLowerCase());
19
+ // Also add short name for scoped packages: @aws-sdk/client-s3 → client-s3, aws-sdk
20
+ if (imp.startsWith("@")) {
21
+ const parts = imp.split("/");
22
+ if (parts[1])
23
+ tokens.add(parts[1].toLowerCase());
24
+ tokens.add(parts[0].slice(1).toLowerCase());
25
+ }
26
+ }
27
+ }
28
+ // Config files may contain dependency info (package.json deps etc.)
29
+ for (const cfg of ctx.codeSamples.configFiles) {
30
+ if (cfg.path.endsWith("package.json")) {
31
+ try {
32
+ const pkg = JSON.parse(cfg.content);
33
+ for (const dep of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
34
+ tokens.add(dep.toLowerCase());
35
+ if (dep.startsWith("@")) {
36
+ const parts = dep.split("/");
37
+ if (parts[1])
38
+ tokens.add(parts[1].toLowerCase());
39
+ tokens.add(parts[0].slice(1).toLowerCase());
40
+ }
41
+ }
42
+ }
43
+ catch { /* ignore parse errors */ }
44
+ }
45
+ }
46
+ // Environment variable names
47
+ for (const env of ctx.envs.environments) {
48
+ for (const v of env.variables) {
49
+ tokens.add(v.toLowerCase());
50
+ }
51
+ }
52
+ // Docker compose service names and images
53
+ for (const svc of ctx.infra.docker.services) {
54
+ tokens.add(svc.name.toLowerCase());
55
+ if (svc.image)
56
+ tokens.add(svc.image.toLowerCase().split(":")[0]);
57
+ }
58
+ // Cloud services detected by infra scanner
59
+ for (const s of ctx.infra.cloud.services) {
60
+ tokens.add(s.toLowerCase());
61
+ }
62
+ // External dependencies mentioned in docs
63
+ for (const dep of ctx.docs.externalDependencies) {
64
+ tokens.add(dep.toLowerCase());
65
+ }
66
+ return tokens;
67
+ }
68
+ /**
69
+ * Check if a service/database ID and type have concrete evidence in the static context.
70
+ * Uses fuzzy matching: checks if any evidence token contains or is contained by the service keywords.
71
+ */
72
+ function hasEvidence(id, name, type, evidenceTokens) {
73
+ // Build candidate keywords from the service
74
+ const candidates = [
75
+ id.toLowerCase(),
76
+ name.toLowerCase(),
77
+ type.toLowerCase(),
78
+ // Split hyphenated IDs: "aws-sqs" → ["aws", "sqs"]
79
+ ...id.toLowerCase().split("-"),
80
+ ].filter(Boolean);
81
+ for (const candidate of candidates) {
82
+ for (const token of evidenceTokens) {
83
+ // Direct match or substring match (in both directions)
84
+ if (token === candidate)
85
+ return true;
86
+ if (token.includes(candidate) && candidate.length >= 3)
87
+ return true;
88
+ if (candidate.includes(token) && token.length >= 3)
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ }
3
94
  /**
4
95
  * Merge all pipeline agent outputs into a StaticAnalysisResult
5
96
  * compatible with the existing buildAnalysisFromStatic() in cli/analyze.ts.
6
97
  */
7
- export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMap, validatorOut, incrementalContext) {
98
+ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMap, validatorOut, incrementalContext, enricherOut) {
8
99
  // Start with components from component-identifier
9
100
  const components = [];
10
101
  if (componentId?.components) {
11
102
  for (const c of componentId.components) {
12
103
  components.push({
13
104
  id: c.id,
14
- name: c.name,
105
+ name: sanitize(c.name) ?? c.name,
15
106
  type: c.type,
16
107
  layer: c.layer,
17
108
  path: c.path,
18
- description: c.description,
109
+ description: sanitize(c.description),
19
110
  technologies: c.technologies,
20
111
  });
21
112
  }
@@ -36,38 +127,44 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
36
127
  }
37
128
  }
38
129
  const componentIds = new Set(components.map((c) => c.id));
39
- // Add databases from service-describer as components
130
+ // Build evidence tokens for hallucination gating
131
+ const evidenceTokens = buildEvidenceTokens(ctx);
132
+ // Add databases from service-describer as components (evidence-gated)
40
133
  if (serviceDesc?.databases) {
41
134
  for (const db of serviceDesc.databases) {
42
- if (!componentIds.has(db.id)) {
43
- components.push({
44
- id: db.id,
45
- name: db.name,
46
- type: "database",
47
- layer: "data",
48
- path: "",
49
- description: db.description,
50
- technologies: [db.type],
51
- });
52
- componentIds.add(db.id);
53
- }
135
+ if (componentIds.has(db.id))
136
+ continue;
137
+ if (!hasEvidence(db.id, db.name, db.type, evidenceTokens))
138
+ continue;
139
+ components.push({
140
+ id: db.id,
141
+ name: db.name,
142
+ type: "database",
143
+ layer: "data",
144
+ path: "",
145
+ description: sanitize(db.description),
146
+ technologies: [db.type],
147
+ });
148
+ componentIds.add(db.id);
54
149
  }
55
150
  }
56
- // Add external services from service-describer as components
151
+ // Add external services from service-describer as components (evidence-gated)
57
152
  if (serviceDesc?.externalServices) {
58
153
  for (const svc of serviceDesc.externalServices) {
59
- if (!componentIds.has(svc.id)) {
60
- components.push({
61
- id: svc.id,
62
- name: svc.name,
63
- type: "service",
64
- layer: "external",
65
- path: "",
66
- description: svc.description,
67
- technologies: [svc.type],
68
- });
69
- componentIds.add(svc.id);
70
- }
154
+ if (componentIds.has(svc.id))
155
+ continue;
156
+ if (!hasEvidence(svc.id, svc.name, svc.type, evidenceTokens))
157
+ continue;
158
+ components.push({
159
+ id: svc.id,
160
+ name: sanitize(svc.name) ?? svc.name,
161
+ type: "service",
162
+ layer: "external",
163
+ path: "",
164
+ description: sanitize(svc.description),
165
+ technologies: [svc.type],
166
+ });
167
+ componentIds.add(svc.id);
71
168
  }
72
169
  }
73
170
  // Incremental fallback: if service-describer returned nothing, restore from spec
@@ -96,7 +193,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
96
193
  // Apply validator description improvements
97
194
  if (validatorOut?.componentDescriptions) {
98
195
  for (const comp of components) {
99
- const better = validatorOut.componentDescriptions[comp.id];
196
+ const better = sanitize(validatorOut.componentDescriptions[comp.id]);
100
197
  if (better && better.length > (comp.description?.length ?? 0)) {
101
198
  comp.description = better;
102
199
  }
@@ -118,7 +215,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
118
215
  from: c.from,
119
216
  to: c.to,
120
217
  type: c.type,
121
- description: c.description,
218
+ description: sanitize(c.description),
122
219
  confidence: 80,
123
220
  async: c.async,
124
221
  });
@@ -198,7 +295,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
198
295
  from: c.from,
199
296
  to: c.to,
200
297
  type: c.type,
201
- description: c.description,
298
+ description: sanitize(c.description),
202
299
  confidence: 65,
203
300
  async: c.async,
204
301
  });
@@ -247,7 +344,38 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
247
344
  // Override docs from service-describer
248
345
  const docs = { ...ctx.docs };
249
346
  if (serviceDesc?.projectDescription && serviceDesc.projectDescription.length > (docs.projectDescription?.length ?? 0)) {
250
- docs.projectDescription = serviceDesc.projectDescription;
347
+ docs.projectDescription = sanitize(serviceDesc.projectDescription);
348
+ }
349
+ // Apply enricher overlays (non-destructive)
350
+ if (enricherOut) {
351
+ // Overlay coupling weight on connections
352
+ if (enricherOut.connectionEnrichments?.length) {
353
+ for (const ce of enricherOut.connectionEnrichments) {
354
+ const conn = filteredConnections.find((c) => c.from === ce.from && c.to === ce.to && c.type === ce.type);
355
+ if (conn) {
356
+ if (ce.weight != null)
357
+ conn.weight = ce.weight;
358
+ if (ce.verifiedByTool)
359
+ conn.confidence = Math.min(100, (conn.confidence ?? 80) + 10);
360
+ }
361
+ }
362
+ }
363
+ // Overlay component enrichments
364
+ if (enricherOut.componentEnrichments?.length) {
365
+ for (const ce of enricherOut.componentEnrichments) {
366
+ const comp = components.find((c) => c.id === ce.id);
367
+ if (comp) {
368
+ if (ce.interfaceSurface != null)
369
+ comp.interfaceSurface = ce.interfaceSurface;
370
+ if (ce.hasBoundary != null)
371
+ comp.hasBoundary = ce.hasBoundary;
372
+ if (ce.boundaryType)
373
+ comp.boundaryType = ce.boundaryType;
374
+ if (ce.publicExports?.length)
375
+ comp.publicExports = ce.publicExports;
376
+ }
377
+ }
378
+ }
251
379
  }
252
380
  return {
253
381
  structure,
@@ -259,13 +387,15 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
259
387
  connections: {
260
388
  connections: filteredConnections,
261
389
  flows: flows.map((f) => ({
262
- name: f.name,
263
- description: f.description,
390
+ name: sanitize(f.name) ?? f.name,
391
+ description: sanitize(f.description),
264
392
  category: f.category,
265
393
  steps: f.steps,
266
394
  })),
267
395
  },
268
396
  validation: { valid: true, repairs: [], errors: [] },
269
397
  gaps: [],
398
+ enrichmentInsights: enricherOut?.insights,
399
+ flowVerifications: enricherOut?.flowVerifications,
270
400
  };
271
401
  }
@@ -5,7 +5,11 @@ export interface PipelineAgent {
5
5
  name: string;
6
6
  modelTier: ModelTier;
7
7
  phase: "parallel" | "sequential";
8
- buildPrompt(ctx: StaticContext, priorResults?: Record<string, unknown>): {
8
+ /** Tool names this agent can use: ["read_file", "grep", "glob"] */
9
+ tools?: string[];
10
+ /** Max tool loop iterations (default 1 = no tools) */
11
+ maxTurns?: number;
12
+ buildPrompt(ctx: StaticContext, priorResults?: Record<string, unknown>, toolsAvailable?: boolean): {
9
13
  system: string;
10
14
  user: string;
11
15
  };
@@ -135,6 +139,30 @@ export interface IncrementalContext {
135
139
  neighborComponents: string[];
136
140
  hasUnmappedFiles: boolean;
137
141
  }
142
+ export interface ArchitectureEnricherOutput {
143
+ connectionEnrichments: Array<{
144
+ from: string;
145
+ to: string;
146
+ type: string;
147
+ weight?: number;
148
+ verifiedByTool?: boolean;
149
+ }>;
150
+ componentEnrichments: Array<{
151
+ id: string;
152
+ interfaceSurface?: number;
153
+ hasBoundary?: boolean;
154
+ boundaryType?: string;
155
+ publicExports?: string[];
156
+ }>;
157
+ flowVerifications: Array<{
158
+ flowName: string;
159
+ verified: boolean;
160
+ stepsVerified: number;
161
+ stepsTotal: number;
162
+ issues?: string[];
163
+ }>;
164
+ insights: string[];
165
+ }
138
166
  export interface PipelineAgentResult {
139
167
  agentId: string;
140
168
  data: unknown;
@@ -1,3 +1,2 @@
1
1
  // Pipeline Agent Framework — Types
2
- // Simpler than ArchByteAgent: no tools, just prompt → JSON
3
2
  export {};
@@ -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,83 @@
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
+ // Dynamic tool config: empty = text-only, populated = tool-enabled
7
+ const sdkTools = params.sdkTools?.length ? params.sdkTools : [];
8
+ const maxTurns = params.sdkMaxTurns ?? 1;
9
+ const result = query({
10
+ prompt,
11
+ options: {
12
+ systemPrompt: params.system,
13
+ tools: sdkTools,
14
+ maxTurns,
15
+ ...(params.model ? { model: params.model } : {}),
16
+ permissionMode: "bypassPermissions",
17
+ allowDangerouslySkipPermissions: true,
18
+ },
19
+ });
20
+ let resultText = "";
21
+ let assistantText = "";
22
+ let usage = { inputTokens: 0, outputTokens: 0 };
23
+ for await (const message of result) {
24
+ if (message.type === "assistant") {
25
+ // Capture text from assistant content blocks as fallback
26
+ const msg = message.message;
27
+ if (msg && "content" in msg && Array.isArray(msg.content)) {
28
+ for (const block of msg.content) {
29
+ if (block.type === "text" && typeof block.text === "string") {
30
+ assistantText += block.text;
31
+ }
32
+ }
33
+ }
34
+ }
35
+ else if (message.type === "result") {
36
+ if (message.subtype === "success") {
37
+ resultText = message.result;
38
+ }
39
+ else {
40
+ // Error result — log the errors for debugging
41
+ const errors = "errors" in message ? message.errors : [];
42
+ console.error(`[claude-sdk] Result subtype="${message.subtype}" errors=${JSON.stringify(errors)}`);
43
+ }
44
+ if (message.usage) {
45
+ usage = {
46
+ inputTokens: message.usage.input_tokens ?? 0,
47
+ outputTokens: message.usage.output_tokens ?? 0,
48
+ };
49
+ }
50
+ }
51
+ }
52
+ // Use result text if available, otherwise fall back to assistant message text
53
+ const text = resultText || assistantText;
54
+ return {
55
+ content: [{ type: "text", text }],
56
+ stopReason: "end_turn",
57
+ usage,
58
+ };
59
+ }
60
+ async *stream(params) {
61
+ // Pipeline agents don't use streaming — delegate to chat()
62
+ const response = await this.chat(params);
63
+ const text = response.content.find((b) => b.type === "text")?.text ?? "";
64
+ if (text) {
65
+ yield { type: "text", text };
66
+ }
67
+ yield { type: "done" };
68
+ }
69
+ extractPrompt(messages) {
70
+ for (let i = messages.length - 1; i >= 0; i--) {
71
+ if (messages[i].role === "user") {
72
+ const content = messages[i].content;
73
+ if (typeof content === "string")
74
+ return content;
75
+ return content
76
+ .filter((b) => b.type === "text")
77
+ .map((b) => b.text ?? "")
78
+ .join("\n");
79
+ }
80
+ }
81
+ return "";
82
+ }
83
+ }
@@ -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
  }
@@ -23,6 +23,10 @@ export interface ChatParams {
23
23
  messages: Message[];
24
24
  tools?: ToolDefinition[];
25
25
  maxTokens?: number;
26
+ /** Claude SDK tool names: ["Read", "Grep", "Glob"] */
27
+ sdkTools?: string[];
28
+ /** Claude SDK max agentic turns (default 1) */
29
+ sdkMaxTurns?: number;
26
30
  }
27
31
  export interface LLMResponse {
28
32
  content: ContentBlock[];
@@ -97,14 +101,14 @@ export interface License {
97
101
  expiresAt: string;
98
102
  isValid: boolean;
99
103
  }
100
- export type ProviderName = "anthropic" | "openai" | "google";
104
+ export type ProviderName = "anthropic" | "openai" | "google" | "claude-sdk";
101
105
  export interface ProviderProfile {
102
106
  apiKey?: string;
103
107
  model?: string;
104
108
  }
105
109
  export interface ArchByteConfig {
106
110
  provider: ProviderName;
107
- apiKey: string;
111
+ apiKey?: string;
108
112
  model?: string;
109
113
  modelOverrides?: Partial<Record<ModelTier, string>>;
110
114
  profiles?: Record<string, ProviderProfile>;