@pruddiman/hem 0.0.1-beta-1aff12a → 0.0.1-beta-95d42fc
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/agents/base-agent.js +2 -10
- package/dist/agents/base-arbiter.js +8 -16
- package/dist/auth.d.ts +14 -1
- package/dist/auth.js +30 -24
- package/dist/grouping.js +6 -17
- package/dist/helpers/parsing.d.ts +0 -33
- package/dist/helpers/parsing.js +0 -80
- package/dist/helpers/paths.d.ts +0 -10
- package/dist/helpers/paths.js +0 -17
- package/dist/helpers/strings.d.ts +0 -7
- package/dist/helpers/strings.js +0 -14
- package/dist/import-graph.d.ts +0 -13
- package/dist/import-graph.js +0 -104
- package/dist/index.d.ts +22 -4
- package/dist/index.js +131 -12
- package/dist/orchestrator.d.ts +11 -4
- package/dist/orchestrator.js +13 -7
- package/dist/progress.d.ts +9 -0
- package/dist/progress.js +36 -7
- package/dist/providers/copilot.d.ts +15 -4
- package/dist/providers/copilot.js +10 -10
- package/dist/providers/index.d.ts +36 -3
- package/dist/providers/index.js +40 -3
- package/dist/providers/types.d.ts +8 -2
- package/dist/server-utils.d.ts +25 -0
- package/dist/server-utils.js +42 -0
- package/package.json +4 -3
- package/dist/agents/grouping-agent.d.ts +0 -167
- package/dist/agents/grouping-agent.js +0 -557
- package/dist/grouping-priors.d.ts +0 -54
- package/dist/grouping-priors.js +0 -130
- package/dist/helpers/index.d.ts +0 -11
- package/dist/helpers/index.js +0 -11
- package/dist/merge-utils.d.ts +0 -22
- package/dist/merge-utils.js +0 -34
- package/dist/session.d.ts +0 -227
- package/dist/session.js +0 -364
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Low-level session ops (`promptAsync`, `session.abort`, `event.subscribe`)
|
|
15
15
|
* are accessed via `this.provider.session.*` and `this.provider.event.*`.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { wrapAuthError } from "../auth.js";
|
|
18
18
|
// ── Base Agent ──────────────────────────────────────────────────────────
|
|
19
19
|
/**
|
|
20
20
|
* Abstract base class providing shared session lifecycle management
|
|
@@ -44,14 +44,6 @@ export class BaseAgent {
|
|
|
44
44
|
* @throws {Error} If creation fails for any other reason.
|
|
45
45
|
*/
|
|
46
46
|
async createSession(_title) {
|
|
47
|
-
|
|
48
|
-
return await this.provider.createSession();
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
if (isAuthExpired(err)) {
|
|
52
|
-
throw new AuthExpiredError("the configured provider", err);
|
|
53
|
-
}
|
|
54
|
-
throw err;
|
|
55
|
-
}
|
|
47
|
+
return wrapAuthError(() => this.provider.createSession());
|
|
56
48
|
}
|
|
57
49
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `BaseArbiter` factors out the lifecycle skeleton; subclasses provide
|
|
22
22
|
* the four configuration values above.
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import { wrapAuthError } from "../auth.js";
|
|
25
25
|
import { BaseAgent } from "./base-agent.js";
|
|
26
26
|
/**
|
|
27
27
|
* Abstract base for arbiter coordinator agents. Concrete subclasses
|
|
@@ -51,21 +51,13 @@ export class BaseArbiter extends BaseAgent {
|
|
|
51
51
|
if (verbose) {
|
|
52
52
|
verbose(`[${this.tag}] session ${sessionId}`);
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
if (isAuthExpired(err)) {
|
|
65
|
-
throw new AuthExpiredError("the configured provider", err);
|
|
66
|
-
}
|
|
67
|
-
throw err;
|
|
68
|
-
}
|
|
54
|
+
await wrapAuthError(() => this.provider.session.promptAsync({
|
|
55
|
+
path: { id: sessionId },
|
|
56
|
+
body: {
|
|
57
|
+
parts: [{ type: "text", text: prompt }],
|
|
58
|
+
agent: this.agentField,
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
69
61
|
if (verbose) {
|
|
70
62
|
verbose(`[${this.tag}] initial prompt sent (listening for broadcasts)`);
|
|
71
63
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -162,6 +162,14 @@ export declare class AuthExpiredError extends Error {
|
|
|
162
162
|
readonly providerName: string;
|
|
163
163
|
constructor(providerName: string, cause?: unknown);
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Run an async operation, converting auth-expiry failures into
|
|
167
|
+
* {@link AuthExpiredError}. Other errors propagate unchanged.
|
|
168
|
+
*
|
|
169
|
+
* Used by call sites that perform a single provider API call and want
|
|
170
|
+
* uniform auth-error handling.
|
|
171
|
+
*/
|
|
172
|
+
export declare function wrapAuthError<T>(op: () => Promise<T>): Promise<T>;
|
|
165
173
|
/**
|
|
166
174
|
* Determine whether an error indicates expired or invalid authentication.
|
|
167
175
|
*
|
|
@@ -364,7 +372,12 @@ export declare function handleAuthLogin(client: OpencodeClient, configDir?: stri
|
|
|
364
372
|
*
|
|
365
373
|
* Reference: FR-021, contracts/cli-interface.md lines 132-143.
|
|
366
374
|
*/
|
|
367
|
-
|
|
375
|
+
/** Per-backbone availability map injected into {@link handleAuthList}. */
|
|
376
|
+
export interface AuthListBackboneAvailability {
|
|
377
|
+
opencode: boolean;
|
|
378
|
+
copilot: boolean;
|
|
379
|
+
}
|
|
380
|
+
export declare function handleAuthList(client: OpencodeClient, configDir?: string, writeFn?: (msg: string) => void, availability?: AuthListBackboneAvailability): Promise<void>;
|
|
368
381
|
/**
|
|
369
382
|
* Handle `hem auth logout` — remove credentials.
|
|
370
383
|
*
|
package/dist/auth.js
CHANGED
|
@@ -430,6 +430,24 @@ export class AuthExpiredError extends Error {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Run an async operation, converting auth-expiry failures into
|
|
435
|
+
* {@link AuthExpiredError}. Other errors propagate unchanged.
|
|
436
|
+
*
|
|
437
|
+
* Used by call sites that perform a single provider API call and want
|
|
438
|
+
* uniform auth-error handling.
|
|
439
|
+
*/
|
|
440
|
+
export async function wrapAuthError(op) {
|
|
441
|
+
try {
|
|
442
|
+
return await op();
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
if (isAuthExpired(err)) {
|
|
446
|
+
throw new AuthExpiredError("the configured provider", err);
|
|
447
|
+
}
|
|
448
|
+
throw err;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
433
451
|
/**
|
|
434
452
|
* Determine whether an error indicates expired or invalid authentication.
|
|
435
453
|
*
|
|
@@ -1007,29 +1025,7 @@ function formatAuthMethod(authMethod) {
|
|
|
1007
1025
|
return "(env var)";
|
|
1008
1026
|
}
|
|
1009
1027
|
}
|
|
1010
|
-
|
|
1011
|
-
* Handle `hem auth list` — show connected providers with status.
|
|
1012
|
-
*
|
|
1013
|
-
* Calls `client.config.providers()`, formats and prints connected providers
|
|
1014
|
-
* with status (checkmark + name + auth method) per `contracts/cli-interface.md`
|
|
1015
|
-
* lines 132-143, also shows the default model from preferences.
|
|
1016
|
-
*
|
|
1017
|
-
* Output format:
|
|
1018
|
-
* ```
|
|
1019
|
-
* Connected providers:
|
|
1020
|
-
* ✓ anthropic Claude Pro/Max (OAuth)
|
|
1021
|
-
* ✓ opencode Zen API Key
|
|
1022
|
-
*
|
|
1023
|
-
* Default model: anthropic/claude-sonnet-4
|
|
1024
|
-
* ```
|
|
1025
|
-
*
|
|
1026
|
-
* @param client — OpenCode SDK client instance.
|
|
1027
|
-
* @param configDir — Override for the config directory (used in tests).
|
|
1028
|
-
* @param writeFn — Override for the output function (used in tests).
|
|
1029
|
-
*
|
|
1030
|
-
* Reference: FR-021, contracts/cli-interface.md lines 132-143.
|
|
1031
|
-
*/
|
|
1032
|
-
export async function handleAuthList(client, configDir, writeFn = (msg) => process.stdout.write(msg)) {
|
|
1028
|
+
export async function handleAuthList(client, configDir, writeFn = (msg) => process.stdout.write(msg), availability) {
|
|
1033
1029
|
// Query the SDK for connected providers.
|
|
1034
1030
|
const result = await client.config.providers();
|
|
1035
1031
|
const sdkProviders = result.data?.providers ?? [];
|
|
@@ -1057,7 +1053,17 @@ export async function handleAuthList(client, configDir, writeFn = (msg) => proce
|
|
|
1057
1053
|
for (const provider of connectedProviders) {
|
|
1058
1054
|
const padding = " ".repeat(Math.max(1, maxIdLen - provider.id.length + 4));
|
|
1059
1055
|
const method = formatAuthMethod(provider.authMethod);
|
|
1060
|
-
|
|
1056
|
+
// Annotate copilot-backed providers as ✗ when the copilot CLI is
|
|
1057
|
+
// missing so the user knows the credential alone won't get them
|
|
1058
|
+
// through generation. Opencode-backed providers are always reachable
|
|
1059
|
+
// here because handleAuthList only runs when opencode is present.
|
|
1060
|
+
const isCopilot = provider.id === "github-copilot" || provider.id === "copilot";
|
|
1061
|
+
const reachable = !isCopilot || availability?.copilot !== false;
|
|
1062
|
+
const icon = reachable ? "\u2713" : "\u2717";
|
|
1063
|
+
const suffix = reachable
|
|
1064
|
+
? ""
|
|
1065
|
+
: " (copilot CLI not installed — npm install -g @github/copilot)";
|
|
1066
|
+
writeFn(` ${icon} ${provider.id}${padding}${provider.name} ${method}${suffix}\n`);
|
|
1061
1067
|
}
|
|
1062
1068
|
}
|
|
1063
1069
|
writeFn("\n");
|
package/dist/grouping.js
CHANGED
|
@@ -66,7 +66,9 @@ export function matchDocFolder(filePath, priors) {
|
|
|
66
66
|
if (priors.length === 0)
|
|
67
67
|
return null;
|
|
68
68
|
const segments = filePath.split("/").map((s) => s.toLowerCase());
|
|
69
|
-
const
|
|
69
|
+
const lastSegment = segments[segments.length - 1] ?? "";
|
|
70
|
+
const dotIndex = lastSegment.indexOf(".");
|
|
71
|
+
const fileStem = (dotIndex === -1 ? lastSegment : lastSegment.substring(0, dotIndex)).toLowerCase();
|
|
70
72
|
// Prefer the prior with the longest key match to avoid `auth` stealing
|
|
71
73
|
// files that should go to `authentication`.
|
|
72
74
|
let best = null;
|
|
@@ -90,10 +92,6 @@ export function normalizeFolderKey(name) {
|
|
|
90
92
|
.replace(/-+/g, "-")
|
|
91
93
|
.replace(/^-|-$/g, "");
|
|
92
94
|
}
|
|
93
|
-
function extractStem(basename) {
|
|
94
|
-
const dotIndex = basename.indexOf(".");
|
|
95
|
-
return (dotIndex === -1 ? basename : basename.substring(0, dotIndex)).toLowerCase();
|
|
96
|
-
}
|
|
97
95
|
/**
|
|
98
96
|
* Scoring:
|
|
99
97
|
* - 3 for a directory-segment match (strongest signal).
|
|
@@ -244,17 +242,6 @@ export function commonDirectory(files) {
|
|
|
244
242
|
}
|
|
245
243
|
return common.length === 0 ? "." : common.join("/");
|
|
246
244
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Returns the "name" portion of the file without known layer suffixes
|
|
249
|
-
* and without the actual file extension.
|
|
250
|
-
* E.g. `user.controller.ts` → the layer suffix is `.controller` → stem `user`.
|
|
251
|
-
* `helpers.ts` → no layer suffix → stem `helpers`.
|
|
252
|
-
*/
|
|
253
|
-
function fileNameWithoutExtension(relativePath) {
|
|
254
|
-
const base = relativePath.split("/").pop() ?? "";
|
|
255
|
-
const lastDot = base.lastIndexOf(".");
|
|
256
|
-
return lastDot === -1 ? base : base.substring(0, lastDot);
|
|
257
|
-
}
|
|
258
245
|
/**
|
|
259
246
|
* Detects the architectural layer for a file by checking its name
|
|
260
247
|
* against known suffixes.
|
|
@@ -262,7 +249,9 @@ function fileNameWithoutExtension(relativePath) {
|
|
|
262
249
|
* @returns The layer label (e.g., "Controllers") or `undefined`.
|
|
263
250
|
*/
|
|
264
251
|
function detectLayer(relativePath) {
|
|
265
|
-
const
|
|
252
|
+
const base = relativePath.split("/").pop() ?? "";
|
|
253
|
+
const lastDot = base.lastIndexOf(".");
|
|
254
|
+
const nameNoExt = (lastDot === -1 ? base : base.substring(0, lastDot)).toLowerCase();
|
|
266
255
|
for (const { suffix, label } of LAYER_SUFFIXES) {
|
|
267
256
|
if (nameNoExt.endsWith(suffix)) {
|
|
268
257
|
return label;
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Text extraction and parsing utilities for Hem.
|
|
3
|
-
*
|
|
4
|
-
* Provides helpers for extracting structured content (Markdown, JSON,
|
|
5
|
-
* file lists, concepts) from LLM response text.
|
|
6
3
|
*/
|
|
7
|
-
import type { MessagePart } from "../session.js";
|
|
8
|
-
/**
|
|
9
|
-
* Extracts the Markdown content from the OpenCode prompt response.
|
|
10
|
-
*
|
|
11
|
-
* Concatenates all text parts from the response, stripping any wrapping
|
|
12
|
-
* code fences if the LLM returned the content inside a fenced block.
|
|
13
|
-
*
|
|
14
|
-
* @param parts - The response parts from `promptAndWait()`.
|
|
15
|
-
* @returns The extracted Markdown string.
|
|
16
|
-
*/
|
|
17
|
-
export declare function extractMarkdown(parts: Array<MessagePart>): string;
|
|
18
4
|
/**
|
|
19
5
|
* Extracts a JSON string from an LLM response that may contain preamble text.
|
|
20
6
|
*
|
|
@@ -31,22 +17,3 @@ export declare function extractMarkdown(parts: Array<MessagePart>): string;
|
|
|
31
17
|
* @returns The extracted JSON substring (not yet parsed).
|
|
32
18
|
*/
|
|
33
19
|
export declare function extractJSON(response: string): string;
|
|
34
|
-
/**
|
|
35
|
-
* Extracts `relatedFiles` from generated Markdown content.
|
|
36
|
-
*
|
|
37
|
-
* Looks for the "Source Files" or "Related Files" section and extracts file
|
|
38
|
-
* paths from backtick-quoted entries (e.g., `` `src/user/controller.ts` ``).
|
|
39
|
-
*
|
|
40
|
-
* @param content - The generated Markdown content.
|
|
41
|
-
* @returns Array of relative file paths found in the Source Files section.
|
|
42
|
-
*/
|
|
43
|
-
export declare function parseRelatedFiles(content: string): string[];
|
|
44
|
-
/**
|
|
45
|
-
* Extracts key concept names from section content.
|
|
46
|
-
*
|
|
47
|
-
* Looks for backtick-quoted identifiers as a heuristic for concepts.
|
|
48
|
-
*
|
|
49
|
-
* @param content - The section body text.
|
|
50
|
-
* @returns Array of unique concept strings.
|
|
51
|
-
*/
|
|
52
|
-
export declare function extractConcepts(content: string): string[];
|
package/dist/helpers/parsing.js
CHANGED
|
@@ -1,36 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Text extraction and parsing utilities for Hem.
|
|
3
|
-
*
|
|
4
|
-
* Provides helpers for extracting structured content (Markdown, JSON,
|
|
5
|
-
* file lists, concepts) from LLM response text.
|
|
6
3
|
*/
|
|
7
|
-
/**
|
|
8
|
-
* Extracts the Markdown content from the OpenCode prompt response.
|
|
9
|
-
*
|
|
10
|
-
* Concatenates all text parts from the response, stripping any wrapping
|
|
11
|
-
* code fences if the LLM returned the content inside a fenced block.
|
|
12
|
-
*
|
|
13
|
-
* @param parts - The response parts from `promptAndWait()`.
|
|
14
|
-
* @returns The extracted Markdown string.
|
|
15
|
-
*/
|
|
16
|
-
export function extractMarkdown(parts) {
|
|
17
|
-
const textParts = parts.filter((p) => p.type === "text" && typeof p.text === "string");
|
|
18
|
-
let content = textParts.map((p) => p.text).join("\n");
|
|
19
|
-
// Strip wrapping ```markdown ... ``` fences if present
|
|
20
|
-
const fencePattern = /^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/;
|
|
21
|
-
const match = content.match(fencePattern);
|
|
22
|
-
if (match) {
|
|
23
|
-
content = match[1];
|
|
24
|
-
}
|
|
25
|
-
// Strip preamble text before the first Markdown heading.
|
|
26
|
-
// LLMs sometimes produce commentary like "Now I have a thorough
|
|
27
|
-
// understanding of the codebase..." before the actual document.
|
|
28
|
-
const h1Match = content.search(/^# .+/m);
|
|
29
|
-
if (h1Match > 0) {
|
|
30
|
-
content = content.slice(h1Match);
|
|
31
|
-
}
|
|
32
|
-
return content.trim();
|
|
33
|
-
}
|
|
34
4
|
/**
|
|
35
5
|
* Extracts a JSON string from an LLM response that may contain preamble text.
|
|
36
6
|
*
|
|
@@ -76,53 +46,3 @@ export function extractJSON(response) {
|
|
|
76
46
|
// Strategy 3: return full string (caller's JSON.parse will validate)
|
|
77
47
|
return response;
|
|
78
48
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Extracts `relatedFiles` from generated Markdown content.
|
|
81
|
-
*
|
|
82
|
-
* Looks for the "Source Files" or "Related Files" section and extracts file
|
|
83
|
-
* paths from backtick-quoted entries (e.g., `` `src/user/controller.ts` ``).
|
|
84
|
-
*
|
|
85
|
-
* @param content - The generated Markdown content.
|
|
86
|
-
* @returns Array of relative file paths found in the Source Files section.
|
|
87
|
-
*/
|
|
88
|
-
export function parseRelatedFiles(content) {
|
|
89
|
-
const files = [];
|
|
90
|
-
const lines = content.split("\n");
|
|
91
|
-
let inSection = false;
|
|
92
|
-
for (const line of lines) {
|
|
93
|
-
// Detect start of Source Files / Related Files section
|
|
94
|
-
if (/^##\s+(Source|Related) Files/i.test(line)) {
|
|
95
|
-
inSection = true;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
// End section at next heading
|
|
99
|
-
if (inSection && /^##\s+/.test(line)) {
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
// Extract file paths from backtick-quoted list items
|
|
103
|
-
if (inSection) {
|
|
104
|
-
const match = line.match(/`([^`]+)`/);
|
|
105
|
-
if (match) {
|
|
106
|
-
files.push(match[1]);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return files;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Extracts key concept names from section content.
|
|
114
|
-
*
|
|
115
|
-
* Looks for backtick-quoted identifiers as a heuristic for concepts.
|
|
116
|
-
*
|
|
117
|
-
* @param content - The section body text.
|
|
118
|
-
* @returns Array of unique concept strings.
|
|
119
|
-
*/
|
|
120
|
-
export function extractConcepts(content) {
|
|
121
|
-
const matches = content.match(/`([^`]+)`/g);
|
|
122
|
-
if (!matches)
|
|
123
|
-
return [];
|
|
124
|
-
const unique = new Set(matches
|
|
125
|
-
.map((m) => m.slice(1, -1))
|
|
126
|
-
.filter((c) => c.length > 0 && !/^\s|\s$/.test(c) && !c.includes(" ")));
|
|
127
|
-
return Array.from(unique);
|
|
128
|
-
}
|
package/dist/helpers/paths.d.ts
CHANGED
|
@@ -29,13 +29,3 @@ export declare function isPathWithin(child: string, parent: string): boolean;
|
|
|
29
29
|
* @throws If the resolved path falls outside the destination directory.
|
|
30
30
|
*/
|
|
31
31
|
export declare function resolveOutputPath(destinationPath: string, relativePath: string): string;
|
|
32
|
-
/**
|
|
33
|
-
* Validates the relationship between destination and source paths.
|
|
34
|
-
*
|
|
35
|
-
* When the destination directory is inside the source directory, logs
|
|
36
|
-
* a note that destination files will be excluded from scanning.
|
|
37
|
-
*
|
|
38
|
-
* @param destinationPath - Path to the destination directory.
|
|
39
|
-
* @param sourcePath - Path to the source directory.
|
|
40
|
-
*/
|
|
41
|
-
export declare function validateDestinationPath(destinationPath: string, sourcePath: string): void;
|
package/dist/helpers/paths.js
CHANGED
|
@@ -48,20 +48,3 @@ export function resolveOutputPath(destinationPath, relativePath) {
|
|
|
48
48
|
}
|
|
49
49
|
return resolved;
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Validates the relationship between destination and source paths.
|
|
53
|
-
*
|
|
54
|
-
* When the destination directory is inside the source directory, logs
|
|
55
|
-
* a note that destination files will be excluded from scanning.
|
|
56
|
-
*
|
|
57
|
-
* @param destinationPath - Path to the destination directory.
|
|
58
|
-
* @param sourcePath - Path to the source directory.
|
|
59
|
-
*/
|
|
60
|
-
export function validateDestinationPath(destinationPath, sourcePath) {
|
|
61
|
-
const absoluteDestination = resolve(destinationPath);
|
|
62
|
-
const absoluteSource = resolve(sourcePath);
|
|
63
|
-
if (isPathWithin(absoluteDestination, absoluteSource)) {
|
|
64
|
-
console.log(`Note: Destination "${absoluteDestination}" is inside source "${absoluteSource}". ` +
|
|
65
|
-
`Destination files will be excluded from scanning.`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -13,13 +13,6 @@
|
|
|
13
13
|
* @returns kebab-cased string.
|
|
14
14
|
*/
|
|
15
15
|
export declare function toKebabCase(input: string): string;
|
|
16
|
-
/**
|
|
17
|
-
* Converts a heading string to a URL-friendly slug.
|
|
18
|
-
*
|
|
19
|
-
* @param heading - The heading text.
|
|
20
|
-
* @returns A kebab-case slug (e.g., "Related Files" → "related-files").
|
|
21
|
-
*/
|
|
22
|
-
export declare function toSlug(heading: string): string;
|
|
23
16
|
/**
|
|
24
17
|
* Converts a directory name to a section heading.
|
|
25
18
|
*
|
package/dist/helpers/strings.js
CHANGED
|
@@ -21,20 +21,6 @@ export function toKebabCase(input) {
|
|
|
21
21
|
.replace(/-+/g, "-")
|
|
22
22
|
.replace(/^-|-$/g, "");
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Converts a heading string to a URL-friendly slug.
|
|
26
|
-
*
|
|
27
|
-
* @param heading - The heading text.
|
|
28
|
-
* @returns A kebab-case slug (e.g., "Related Files" → "related-files").
|
|
29
|
-
*/
|
|
30
|
-
export function toSlug(heading) {
|
|
31
|
-
return heading
|
|
32
|
-
.toLowerCase()
|
|
33
|
-
.replace(/[^a-z0-9\s-]/g, "")
|
|
34
|
-
.replace(/\s+/g, "-")
|
|
35
|
-
.replace(/-+/g, "-")
|
|
36
|
-
.replace(/^-|-$/g, "");
|
|
37
|
-
}
|
|
38
24
|
/**
|
|
39
25
|
* Converts a directory name to a section heading.
|
|
40
26
|
*
|
package/dist/import-graph.d.ts
CHANGED
|
@@ -45,19 +45,6 @@ export declare function buildImportGraph(files: FileInfo[]): Promise<ImportAnaly
|
|
|
45
45
|
* isolated nodes appear as singleton components.
|
|
46
46
|
*/
|
|
47
47
|
export declare function connectedComponents(universe: readonly string[], localEdges: Map<string, string[]>): string[][];
|
|
48
|
-
/**
|
|
49
|
-
* Compute fan-in (how many files import this file) and fan-out (how many
|
|
50
|
-
* files this file imports) for every node in `universe`.
|
|
51
|
-
*/
|
|
52
|
-
export declare function computeDegrees(universe: readonly string[], localEdges: Map<string, string[]>): Map<string, {
|
|
53
|
-
fanIn: number;
|
|
54
|
-
fanOut: number;
|
|
55
|
-
}>;
|
|
56
|
-
/**
|
|
57
|
-
* Identify files participating in an import cycle. Uses iterative Tarjan's
|
|
58
|
-
* SCC; any SCC with ≥2 members or a self-loop marks its members as cyclic.
|
|
59
|
-
*/
|
|
60
|
-
export declare function nodesInCycles(localEdges: Map<string, string[]>): Set<string>;
|
|
61
48
|
/**
|
|
62
49
|
* Yield every import specifier found in `content` along with the 1-based
|
|
63
50
|
* line number it appears on.
|
package/dist/import-graph.js
CHANGED
|
@@ -130,110 +130,6 @@ export function connectedComponents(universe, localEdges) {
|
|
|
130
130
|
}
|
|
131
131
|
return [...components.values()].map((c) => c.slice().sort((a, b) => a.localeCompare(b)));
|
|
132
132
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Compute fan-in (how many files import this file) and fan-out (how many
|
|
135
|
-
* files this file imports) for every node in `universe`.
|
|
136
|
-
*/
|
|
137
|
-
export function computeDegrees(universe, localEdges) {
|
|
138
|
-
const degrees = new Map();
|
|
139
|
-
for (const node of universe) {
|
|
140
|
-
degrees.set(node, { fanIn: 0, fanOut: 0 });
|
|
141
|
-
}
|
|
142
|
-
for (const [from, tos] of localEdges) {
|
|
143
|
-
const d = degrees.get(from);
|
|
144
|
-
if (!d)
|
|
145
|
-
continue;
|
|
146
|
-
d.fanOut = tos.length;
|
|
147
|
-
for (const to of tos) {
|
|
148
|
-
const td = degrees.get(to);
|
|
149
|
-
if (td)
|
|
150
|
-
td.fanIn += 1;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return degrees;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Identify files participating in an import cycle. Uses iterative Tarjan's
|
|
157
|
-
* SCC; any SCC with ≥2 members or a self-loop marks its members as cyclic.
|
|
158
|
-
*/
|
|
159
|
-
export function nodesInCycles(localEdges) {
|
|
160
|
-
const index = new Map();
|
|
161
|
-
const lowlink = new Map();
|
|
162
|
-
const onStack = new Set();
|
|
163
|
-
const stack = [];
|
|
164
|
-
const result = new Set();
|
|
165
|
-
let counter = 0;
|
|
166
|
-
const nodes = new Set();
|
|
167
|
-
for (const [from, tos] of localEdges) {
|
|
168
|
-
nodes.add(from);
|
|
169
|
-
for (const to of tos)
|
|
170
|
-
nodes.add(to);
|
|
171
|
-
}
|
|
172
|
-
const strongConnect = (start) => {
|
|
173
|
-
const frames = [];
|
|
174
|
-
index.set(start, counter);
|
|
175
|
-
lowlink.set(start, counter);
|
|
176
|
-
counter++;
|
|
177
|
-
stack.push(start);
|
|
178
|
-
onStack.add(start);
|
|
179
|
-
frames.push({
|
|
180
|
-
node: start,
|
|
181
|
-
iter: (localEdges.get(start) ?? [])[Symbol.iterator](),
|
|
182
|
-
});
|
|
183
|
-
while (frames.length > 0) {
|
|
184
|
-
const frame = frames[frames.length - 1];
|
|
185
|
-
const next = frame.iter.next();
|
|
186
|
-
if (next.done) {
|
|
187
|
-
// Finished with frame.node — check if it's an SCC root.
|
|
188
|
-
if (lowlink.get(frame.node) === index.get(frame.node)) {
|
|
189
|
-
const component = [];
|
|
190
|
-
let w;
|
|
191
|
-
do {
|
|
192
|
-
w = stack.pop();
|
|
193
|
-
onStack.delete(w);
|
|
194
|
-
component.push(w);
|
|
195
|
-
} while (w !== frame.node);
|
|
196
|
-
const neighbours = localEdges.get(frame.node) ?? [];
|
|
197
|
-
const hasSelfLoop = neighbours.includes(frame.node);
|
|
198
|
-
if (component.length >= 2 || hasSelfLoop) {
|
|
199
|
-
for (const m of component)
|
|
200
|
-
result.add(m);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
frames.pop();
|
|
204
|
-
// Propagate lowlink to parent.
|
|
205
|
-
if (frames.length > 0) {
|
|
206
|
-
const parentFrame = frames[frames.length - 1];
|
|
207
|
-
const pl = lowlink.get(parentFrame.node);
|
|
208
|
-
const cl = lowlink.get(frame.node);
|
|
209
|
-
if (cl < pl)
|
|
210
|
-
lowlink.set(parentFrame.node, cl);
|
|
211
|
-
}
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const w = next.value;
|
|
215
|
-
if (!index.has(w)) {
|
|
216
|
-
index.set(w, counter);
|
|
217
|
-
lowlink.set(w, counter);
|
|
218
|
-
counter++;
|
|
219
|
-
stack.push(w);
|
|
220
|
-
onStack.add(w);
|
|
221
|
-
frames.push({ node: w, iter: (localEdges.get(w) ?? [])[Symbol.iterator]() });
|
|
222
|
-
}
|
|
223
|
-
else if (onStack.has(w)) {
|
|
224
|
-
const cur = lowlink.get(frame.node);
|
|
225
|
-
const wIndex = index.get(w);
|
|
226
|
-
if (wIndex < cur)
|
|
227
|
-
lowlink.set(frame.node, wIndex);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
for (const node of nodes) {
|
|
232
|
-
if (!index.has(node))
|
|
233
|
-
strongConnect(node);
|
|
234
|
-
}
|
|
235
|
-
return result;
|
|
236
|
-
}
|
|
237
133
|
// ── Internal helpers ────────────────────────────────────────────────────
|
|
238
134
|
/**
|
|
239
135
|
* Yield every import specifier found in `content` along with the 1-based
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { findFreePort, startWithRetry } from "./server-utils.js";
|
|
|
28
28
|
import { discoverFiles, detectProjectName } from "./discovery.js";
|
|
29
29
|
import { groupFiles } from "./grouping.js";
|
|
30
30
|
import type { Provider } from "./providers/types.js";
|
|
31
|
+
import { getBackboneAvailability } from "./providers/index.js";
|
|
31
32
|
import { generateDocumentation, getExitCode } from "./orchestrator.js";
|
|
32
33
|
export { getExitCode };
|
|
33
34
|
import { renderDashboard } from "./progress.js";
|
|
@@ -78,15 +79,16 @@ export interface Dependencies {
|
|
|
78
79
|
detectChangedDocs: typeof detectChangedDocs;
|
|
79
80
|
writeChangelogEntry: typeof writeChangelogEntry;
|
|
80
81
|
scopeToChangedFiles: typeof scopeToChangedFiles;
|
|
82
|
+
getBackboneAvailability: typeof getBackboneAvailability;
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
83
85
|
* Subset of {@link Dependencies} actually needed by `handleAuth`.
|
|
84
86
|
* Narrower types document what each entry-point actually depends on
|
|
85
87
|
* and let test mocks omit unrelated fields without TypeScript errors.
|
|
86
88
|
*/
|
|
87
|
-
export type AuthDeps = Pick<Dependencies, "startWithRetry" | "findFreePort" | "createOpencode" | "handleAuthLogin" | "handleAuthList" | "handleAuthLogout">;
|
|
89
|
+
export type AuthDeps = Pick<Dependencies, "startWithRetry" | "findFreePort" | "createOpencode" | "handleAuthLogin" | "handleAuthList" | "handleAuthLogout" | "getBackboneAvailability">;
|
|
88
90
|
/** Subset of {@link Dependencies} actually needed by `handleConfig`. */
|
|
89
|
-
export type ConfigDeps = Pick<Dependencies, "startWithRetry" | "findFreePort" | "createOpencode" | "detectAuthState" | "handleFirstRun" | "saveProjectConfig" | "renderAndWait" | "listProviderModels">;
|
|
91
|
+
export type ConfigDeps = Pick<Dependencies, "startWithRetry" | "findFreePort" | "createOpencode" | "detectAuthState" | "handleFirstRun" | "saveProjectConfig" | "renderAndWait" | "listProviderModels" | "getBackboneAvailability">;
|
|
90
92
|
/** Default (production) dependency bag. */
|
|
91
93
|
export declare const defaultDeps: Dependencies;
|
|
92
94
|
/**
|
|
@@ -132,13 +134,29 @@ export declare function handleGenerate(opts: {
|
|
|
132
134
|
}, deps?: Dependencies): Promise<GenerateCLIOptions | null>;
|
|
133
135
|
/**
|
|
134
136
|
* Handle the `auth` subcommand.
|
|
137
|
+
*
|
|
138
|
+
* Gracefully degrades when the opencode CLI is missing: rather than
|
|
139
|
+
* crashing with `spawn opencode ENOENT`, prints a backbone-availability
|
|
140
|
+
* report so the user knows what to install. Login/logout require opencode
|
|
141
|
+
* today (the auth flows are opencode-managed); copilot uses GitHub
|
|
142
|
+
* authentication outside hem (`$GITHUB_TOKEN` or `gh auth login`).
|
|
135
143
|
*/
|
|
136
144
|
export declare function handleAuth(action: string | undefined, target: string | undefined, deps?: AuthDeps): Promise<AuthCLIOptions | null>;
|
|
137
145
|
/**
|
|
138
146
|
* Handle the `config` subcommand.
|
|
139
147
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
148
|
+
* Always opens with a backbone picker that lists every supported
|
|
149
|
+
* provider backbone (currently opencode and copilot) with a green \u2713
|
|
150
|
+
* "installed" or red \u2717 "not installed" indicator alongside an
|
|
151
|
+
* install hint. The user picks an installed backbone; uninstalled ones
|
|
152
|
+
* are visible but not selectable.
|
|
153
|
+
*
|
|
154
|
+
* - Pick `opencode` → start the opencode server, run the existing
|
|
155
|
+
* auth-detect + sub-provider/model selection flow.
|
|
156
|
+
* - Pick `copilot` → save `{providerID: "github-copilot", modelID: "default"}`
|
|
157
|
+
* directly (no opencode server needed).
|
|
158
|
+
* - No backbones installed → print install hints for all of them and
|
|
159
|
+
* exit non-zero.
|
|
142
160
|
*/
|
|
143
161
|
export declare function handleConfig(deps?: ConfigDeps): Promise<ConfigCLIOptions | null>;
|
|
144
162
|
/**
|