@crafter/cli-tree 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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/archaeology/cache.d.ts +11 -0
  4. package/dist/archaeology/delegate.d.ts +43 -0
  5. package/dist/archaeology/index.d.ts +12 -0
  6. package/dist/archaeology/index.js +61 -0
  7. package/dist/archaeology/index.js.map +9 -0
  8. package/dist/archaeology/llm.d.ts +1 -0
  9. package/dist/archaeology/merge.d.ts +3 -0
  10. package/dist/archaeology/orchestrator.d.ts +25 -0
  11. package/dist/archaeology/prompts.d.ts +13 -0
  12. package/dist/archaeology/types.d.ts +101 -0
  13. package/dist/archaeology/validate.d.ts +18 -0
  14. package/dist/chunk-57gtsvhb.js +434 -0
  15. package/dist/chunk-57gtsvhb.js.map +16 -0
  16. package/dist/chunk-5aahbfr2.js +293 -0
  17. package/dist/chunk-5aahbfr2.js.map +10 -0
  18. package/dist/chunk-pkfpaae1.js +678 -0
  19. package/dist/chunk-pkfpaae1.js.map +15 -0
  20. package/dist/chunk-q4se2rwe.js +181 -0
  21. package/dist/chunk-q4se2rwe.js.map +14 -0
  22. package/dist/chunk-v5w3w6bd.js +168 -0
  23. package/dist/chunk-v5w3w6bd.js.map +11 -0
  24. package/dist/chunk-ykze151b.js +770 -0
  25. package/dist/chunk-ykze151b.js.map +16 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +433 -0
  28. package/dist/cli.js.map +10 -0
  29. package/dist/encoders/ansi.d.ts +2 -0
  30. package/dist/encoders/html.d.ts +10 -0
  31. package/dist/encoders/string.d.ts +2 -0
  32. package/dist/flow/encode.d.ts +5 -0
  33. package/dist/flow/index.d.ts +8 -0
  34. package/dist/flow/index.js +25 -0
  35. package/dist/flow/index.js.map +9 -0
  36. package/dist/flow/layout.d.ts +30 -0
  37. package/dist/flow/parse.d.ts +2 -0
  38. package/dist/flow/render.d.ts +3 -0
  39. package/dist/flow/types.d.ts +42 -0
  40. package/dist/flow/validate.d.ts +3 -0
  41. package/dist/flow/yaml.d.ts +4 -0
  42. package/dist/grid.d.ts +14 -0
  43. package/dist/index.d.ts +7 -0
  44. package/dist/index.js +21 -0
  45. package/dist/index.js.map +9 -0
  46. package/dist/miner/history.d.ts +6 -0
  47. package/dist/miner/index.d.ts +18 -0
  48. package/dist/miner/index.js +38 -0
  49. package/dist/miner/index.js.map +9 -0
  50. package/dist/miner/sessions.d.ts +3 -0
  51. package/dist/miner/stats.d.ts +2 -0
  52. package/dist/miner/suggest.d.ts +11 -0
  53. package/dist/miner/transitions.d.ts +6 -0
  54. package/dist/miner/types.d.ts +46 -0
  55. package/dist/miner/workflows.d.ts +11 -0
  56. package/dist/parse.d.ts +3 -0
  57. package/dist/render.d.ts +3 -0
  58. package/dist/types.d.ts +39 -0
  59. package/package.json +85 -0
  60. package/skill/SKILL.md +263 -0
  61. package/skill/evals/evals.json +26 -0
  62. package/skill/install.sh +38 -0
  63. package/skill/references/archaeology-guide.md +157 -0
  64. package/skill/references/skill-template.md +120 -0
  65. package/src/archaeology/cache.ts +107 -0
  66. package/src/archaeology/delegate.ts +113 -0
  67. package/src/archaeology/index.ts +48 -0
  68. package/src/archaeology/llm.ts +10 -0
  69. package/src/archaeology/merge.ts +155 -0
  70. package/src/archaeology/orchestrator.ts +185 -0
  71. package/src/archaeology/prompts.ts +178 -0
  72. package/src/archaeology/types.ts +139 -0
  73. package/src/archaeology/validate.ts +157 -0
  74. package/src/cli.ts +451 -0
  75. package/src/encoders/ansi.ts +32 -0
  76. package/src/encoders/html.ts +78 -0
  77. package/src/encoders/string.ts +20 -0
  78. package/src/flow/encode.ts +21 -0
  79. package/src/flow/index.ts +15 -0
  80. package/src/flow/layout.ts +150 -0
  81. package/src/flow/parse.ts +100 -0
  82. package/src/flow/render.ts +186 -0
  83. package/src/flow/types.ts +45 -0
  84. package/src/flow/validate.ts +111 -0
  85. package/src/flow/yaml.ts +235 -0
  86. package/src/grid.ts +59 -0
  87. package/src/index.ts +24 -0
  88. package/src/miner/history.ts +156 -0
  89. package/src/miner/index.ts +76 -0
  90. package/src/miner/sessions.ts +39 -0
  91. package/src/miner/stats.ts +43 -0
  92. package/src/miner/suggest.ts +101 -0
  93. package/src/miner/transitions.ts +62 -0
  94. package/src/miner/types.ts +45 -0
  95. package/src/miner/workflows.ts +96 -0
  96. package/src/parse.ts +321 -0
  97. package/src/render.ts +182 -0
  98. package/src/types.ts +62 -0
  99. package/workflows/docker-deploy.yml +42 -0
  100. package/workflows/docker-parallel.yml +36 -0
  101. package/workflows/gh-issue-to-pr.yml +48 -0
  102. package/workflows/git-pr-flow.yml +36 -0
  103. package/workflows/kubectl-rollout.yml +37 -0
  104. package/workflows/npm-publish.yml +42 -0
@@ -0,0 +1,107 @@
1
+ import type { ArchaeologyCache, EnrichedCLINode } from "./types";
2
+
3
+ export interface CacheOptions {
4
+ cacheDir?: string;
5
+ ttlDays?: number;
6
+ }
7
+
8
+ function getCacheDir(opts?: CacheOptions): string {
9
+ if (opts?.cacheDir) return opts.cacheDir;
10
+ const home = process.env.HOME ?? "";
11
+ if (!home) throw new Error("HOME is not set; cannot determine cache directory");
12
+ return `${home}/.clitree/cache`;
13
+ }
14
+
15
+ function cachePath(cli: string, opts?: CacheOptions): string {
16
+ const dir = getCacheDir(opts);
17
+ const safe = cli.replace(/[^a-zA-Z0-9_-]/g, "_");
18
+ return `${dir}/${safe}.json`;
19
+ }
20
+
21
+ export async function loadCache(cli: string, opts?: CacheOptions): Promise<ArchaeologyCache | null> {
22
+ const path = cachePath(cli, opts);
23
+ const file = Bun.file(path);
24
+ const exists = await file.exists();
25
+ if (!exists) return null;
26
+
27
+ try {
28
+ const data = await file.json();
29
+ return data as ArchaeologyCache;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export async function saveCache(cli: string, cache: ArchaeologyCache, opts?: CacheOptions): Promise<void> {
36
+ const dir = getCacheDir(opts);
37
+ await ensureDir(dir);
38
+
39
+ const path = cachePath(cli, opts);
40
+ await Bun.write(path, JSON.stringify(cache, null, 2));
41
+ }
42
+
43
+ export async function invalidateCache(cli: string, opts?: CacheOptions): Promise<void> {
44
+ const path = cachePath(cli, opts);
45
+ const file = Bun.file(path);
46
+ if (await file.exists()) {
47
+ await Bun.$`rm ${path}`.quiet();
48
+ }
49
+ }
50
+
51
+ export function isCacheStale(cache: ArchaeologyCache, currentVersion?: string, ttlDays?: number): boolean {
52
+ if (currentVersion && cache.cliVersion && cache.cliVersion !== currentVersion) {
53
+ return true;
54
+ }
55
+
56
+ if (ttlDays) {
57
+ const created = new Date(cache.createdAt).getTime();
58
+ const now = Date.now();
59
+ const ageDays = (now - created) / (1000 * 60 * 60 * 24);
60
+ if (ageDays > ttlDays) return true;
61
+ }
62
+
63
+ return false;
64
+ }
65
+
66
+ export async function detectCliVersion(binary: string): Promise<string | undefined> {
67
+ const candidates = ["--version", "-v", "version"];
68
+ for (const arg of candidates) {
69
+ try {
70
+ const proc = Bun.spawn([binary, arg], { stdout: "pipe", stderr: "pipe" });
71
+ const timeout = setTimeout(() => proc.kill(), 3000);
72
+
73
+ const [stdout, stderr] = await Promise.all([
74
+ new Response(proc.stdout).text(),
75
+ new Response(proc.stderr).text(),
76
+ ]);
77
+ clearTimeout(timeout);
78
+ await proc.exited;
79
+
80
+ const output = (stdout || stderr).trim();
81
+ const match = output.match(/v?\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?/);
82
+ if (match) return match[0];
83
+ } catch {}
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ async function ensureDir(dir: string): Promise<void> {
89
+ try {
90
+ await Bun.$`mkdir -p ${dir}`.quiet();
91
+ } catch {}
92
+ }
93
+
94
+ export function createEmptyCache(cli: string, tree: EnrichedCLINode, cliVersion?: string): ArchaeologyCache {
95
+ return {
96
+ cli,
97
+ cliVersion,
98
+ createdAt: new Date().toISOString(),
99
+ tree,
100
+ phases: {
101
+ survey: false,
102
+ excavation: false,
103
+ deepDive: false,
104
+ cartography: false,
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,113 @@
1
+ import type { DeepDiveProposal, SurveyHypothesis, FlagConstraint } from "./types";
2
+
3
+ export interface ArchaeologyDelegate {
4
+ readonly name: string;
5
+ readonly available: boolean;
6
+
7
+ survey?(cli: string, version?: string): Promise<SurveyHypothesis>;
8
+
9
+ proposeHiddenFlags?(cli: string, subcommand: string[]): Promise<DeepDiveProposal>;
10
+
11
+ proposeConstraints?(cli: string, subcommand: string[]): Promise<FlagConstraint[]>;
12
+ }
13
+
14
+ export class NullDelegate implements ArchaeologyDelegate {
15
+ readonly name = "null";
16
+ readonly available = false;
17
+ }
18
+
19
+ export class BridgeFileDelegate implements ArchaeologyDelegate {
20
+ readonly name = "bridge-file";
21
+ readonly available = true;
22
+ private requestsPath: string;
23
+ private responsesPath: string;
24
+ private timeoutMs: number;
25
+
26
+ constructor(opts: { requestsPath?: string; responsesPath?: string; timeoutMs?: number } = {}) {
27
+ const tmp = process.env.TMPDIR ?? "/tmp";
28
+ this.requestsPath = opts.requestsPath ?? `${tmp}/clitree-requests.jsonl`;
29
+ this.responsesPath = opts.responsesPath ?? `${tmp}/clitree-responses.jsonl`;
30
+ this.timeoutMs = opts.timeoutMs ?? 120_000;
31
+ }
32
+
33
+ async survey(cli: string, version?: string): Promise<SurveyHypothesis> {
34
+ const id = crypto.randomUUID();
35
+ await this.writeRequest({ id, kind: "survey", cli, version });
36
+ const response = await this.awaitResponse(id);
37
+ return response as SurveyHypothesis;
38
+ }
39
+
40
+ async proposeHiddenFlags(cli: string, subcommand: string[]): Promise<DeepDiveProposal> {
41
+ const id = crypto.randomUUID();
42
+ await this.writeRequest({ id, kind: "deep-dive", cli, subcommand });
43
+ const response = await this.awaitResponse(id);
44
+ return response as DeepDiveProposal;
45
+ }
46
+
47
+ async proposeConstraints(cli: string, subcommand: string[]): Promise<FlagConstraint[]> {
48
+ const id = crypto.randomUUID();
49
+ await this.writeRequest({ id, kind: "cartography", cli, subcommand });
50
+ const response = await this.awaitResponse(id);
51
+ return (response as { constraints: FlagConstraint[] }).constraints ?? [];
52
+ }
53
+
54
+ private async writeRequest(req: unknown): Promise<void> {
55
+ const existing = await this.readFile(this.requestsPath);
56
+ const line = JSON.stringify(req) + "\n";
57
+ await Bun.write(this.requestsPath, existing + line, { createPath: true });
58
+ }
59
+
60
+ private async awaitResponse(id: string): Promise<unknown> {
61
+ const start = Date.now();
62
+ while (Date.now() - start < this.timeoutMs) {
63
+ const text = await this.readFile(this.responsesPath);
64
+ const lines = text.split("\n").filter(l => l.trim());
65
+ for (const line of lines) {
66
+ try {
67
+ const parsed = JSON.parse(line);
68
+ if (parsed.id === id) return parsed.result;
69
+ } catch {}
70
+ }
71
+ await new Promise(r => setTimeout(r, 250));
72
+ }
73
+ throw new Error(`Timed out waiting for delegate response to request ${id}`);
74
+ }
75
+
76
+ private async readFile(path: string): Promise<string> {
77
+ try {
78
+ const file = Bun.file(path);
79
+ if (!(await file.exists())) return "";
80
+ return await file.text();
81
+ } catch {
82
+ return "";
83
+ }
84
+ }
85
+ }
86
+
87
+ export class InlineDelegate implements ArchaeologyDelegate {
88
+ readonly name = "inline";
89
+ readonly available = true;
90
+
91
+ constructor(
92
+ private handlers: {
93
+ survey?: (cli: string, version?: string) => Promise<SurveyHypothesis>;
94
+ proposeHiddenFlags?: (cli: string, subcommand: string[]) => Promise<DeepDiveProposal>;
95
+ proposeConstraints?: (cli: string, subcommand: string[]) => Promise<FlagConstraint[]>;
96
+ },
97
+ ) {}
98
+
99
+ async survey(cli: string, version?: string): Promise<SurveyHypothesis> {
100
+ if (!this.handlers.survey) throw new Error("InlineDelegate: survey handler not provided");
101
+ return this.handlers.survey(cli, version);
102
+ }
103
+
104
+ async proposeHiddenFlags(cli: string, subcommand: string[]): Promise<DeepDiveProposal> {
105
+ if (!this.handlers.proposeHiddenFlags) throw new Error("InlineDelegate: proposeHiddenFlags handler not provided");
106
+ return this.handlers.proposeHiddenFlags(cli, subcommand);
107
+ }
108
+
109
+ async proposeConstraints(cli: string, subcommand: string[]): Promise<FlagConstraint[]> {
110
+ if (!this.handlers.proposeConstraints) throw new Error("InlineDelegate: proposeConstraints handler not provided");
111
+ return this.handlers.proposeConstraints(cli, subcommand);
112
+ }
113
+ }
@@ -0,0 +1,48 @@
1
+ export type {
2
+ Source,
3
+ SourceKind,
4
+ EnrichedCLINode,
5
+ EnrichedFlag,
6
+ EnrichedArg,
7
+ Example,
8
+ FlagConstraint,
9
+ SurveyHypothesis,
10
+ DeepDiveProposal,
11
+ ValidationVerdict,
12
+ ArchaeologyCache,
13
+ ArchaeologyOptions,
14
+ } from "./types";
15
+
16
+ export { toCLINode, fromCLINode } from "./types";
17
+
18
+ export {
19
+ surveyPrompt,
20
+ excavationPrompt,
21
+ deepDivePrompt,
22
+ cartographyPrompt,
23
+ validationInstructions,
24
+ extractJsonFromLlmResponse,
25
+ } from "./prompts";
26
+
27
+ export {
28
+ runHelpCommand,
29
+ validateSubcommandExists,
30
+ validateSurvey,
31
+ excavateCommand,
32
+ validateDeepDive,
33
+ summarizeValidation,
34
+ } from "./validate";
35
+
36
+ export type { ValidationSummary } from "./validate";
37
+
38
+ export { mergeNodes, filterUnverified } from "./merge";
39
+
40
+ export { loadCache, saveCache, invalidateCache, isCacheStale, detectCliVersion, createEmptyCache } from "./cache";
41
+
42
+ export type { CacheOptions } from "./cache";
43
+
44
+ export type { ArchaeologyDelegate } from "./delegate";
45
+ export { NullDelegate, BridgeFileDelegate, InlineDelegate } from "./delegate";
46
+
47
+ export { runArchaeology, enrichedToTree } from "./orchestrator";
48
+ export type { ArchaeologyProgress, ArchaeologyResult } from "./orchestrator";
@@ -0,0 +1,10 @@
1
+ // Deprecated module — archaeology LLM calls go through ArchaeologyDelegate
2
+ // (see delegate.ts). This file is kept as a shim for backwards compatibility
3
+ // with the orchestrator import path and will be removed in a future release.
4
+
5
+ export {
6
+ type ArchaeologyDelegate,
7
+ NullDelegate,
8
+ BridgeFileDelegate,
9
+ InlineDelegate,
10
+ } from "./delegate";
@@ -0,0 +1,155 @@
1
+ import type { EnrichedCLINode, EnrichedFlag, EnrichedArg, Source, SourceKind } from "./types";
2
+
3
+ const SOURCE_PRIORITY: Record<SourceKind, number> = {
4
+ "user": 100,
5
+ "help": 90,
6
+ "llm-validated": 70,
7
+ "man": 60,
8
+ "completion": 55,
9
+ "llm-cartography": 40,
10
+ "llm-deep-dive": 35,
11
+ "llm-survey": 20,
12
+ };
13
+
14
+ function sourceRank(source: Source | undefined): number {
15
+ if (!source) return 0;
16
+ return SOURCE_PRIORITY[source.type] ?? 0;
17
+ }
18
+
19
+ export function mergeNodes(a: EnrichedCLINode, b: EnrichedCLINode): EnrichedCLINode {
20
+ if (a.name !== b.name) {
21
+ throw new Error(`Cannot merge nodes with different names: ${a.name} vs ${b.name}`);
22
+ }
23
+
24
+ const highPriority = sourceRank(a.source) >= sourceRank(b.source) ? a : b;
25
+ const lowPriority = highPriority === a ? b : a;
26
+
27
+ const merged: EnrichedCLINode = {
28
+ name: a.name,
29
+ source: highPriority.source,
30
+ };
31
+
32
+ merged.description = highPriority.description ?? lowPriority.description;
33
+
34
+ if (highPriority.aliases || lowPriority.aliases) {
35
+ const set = new Set([...(highPriority.aliases ?? []), ...(lowPriority.aliases ?? [])]);
36
+ merged.aliases = Array.from(set);
37
+ }
38
+
39
+ merged.hidden = highPriority.hidden ?? lowPriority.hidden;
40
+ merged.deprecated = highPriority.deprecated ?? lowPriority.deprecated;
41
+ merged.experimental = highPriority.experimental ?? lowPriority.experimental;
42
+
43
+ merged.flags = mergeFlags(highPriority.flags ?? [], lowPriority.flags ?? []);
44
+ if (merged.flags.length === 0) delete merged.flags;
45
+
46
+ merged.args = mergeArgs(highPriority.args ?? [], lowPriority.args ?? []);
47
+ if (merged.args.length === 0) delete merged.args;
48
+
49
+ merged.subcommands = mergeSubcommands(highPriority.subcommands ?? [], lowPriority.subcommands ?? []);
50
+ if (merged.subcommands.length === 0) delete merged.subcommands;
51
+
52
+ if (highPriority.constraints || lowPriority.constraints) {
53
+ merged.constraints = [...(highPriority.constraints ?? []), ...(lowPriority.constraints ?? [])];
54
+ }
55
+
56
+ if (highPriority.examples || lowPriority.examples) {
57
+ merged.examples = dedupeExamples([...(highPriority.examples ?? []), ...(lowPriority.examples ?? [])]);
58
+ }
59
+
60
+ return merged;
61
+ }
62
+
63
+ function mergeFlags(primary: EnrichedFlag[], secondary: EnrichedFlag[]): EnrichedFlag[] {
64
+ const byName = new Map<string, EnrichedFlag>();
65
+
66
+ for (const flag of primary) {
67
+ byName.set(flag.name, flag);
68
+ }
69
+
70
+ for (const flag of secondary) {
71
+ const existing = byName.get(flag.name);
72
+ if (!existing) {
73
+ byName.set(flag.name, flag);
74
+ } else {
75
+ byName.set(flag.name, mergeFlag(existing, flag));
76
+ }
77
+ }
78
+
79
+ return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
80
+ }
81
+
82
+ function mergeFlag(a: EnrichedFlag, b: EnrichedFlag): EnrichedFlag {
83
+ const high = sourceRank(a.source) >= sourceRank(b.source) ? a : b;
84
+ const low = high === a ? b : a;
85
+
86
+ return {
87
+ ...low,
88
+ ...high,
89
+ description: high.description ?? low.description,
90
+ short: high.short ?? low.short,
91
+ examples: [...(high.examples ?? []), ...(low.examples ?? [])],
92
+ excludes: Array.from(new Set([...(high.excludes ?? []), ...(low.excludes ?? [])])),
93
+ requires: Array.from(new Set([...(high.requires ?? []), ...(low.requires ?? [])])),
94
+ };
95
+ }
96
+
97
+ function mergeArgs(primary: EnrichedArg[], secondary: EnrichedArg[]): EnrichedArg[] {
98
+ const byName = new Map<string, EnrichedArg>();
99
+ for (const arg of primary) byName.set(arg.name, arg);
100
+ for (const arg of secondary) {
101
+ if (!byName.has(arg.name)) byName.set(arg.name, arg);
102
+ }
103
+ return Array.from(byName.values());
104
+ }
105
+
106
+ function mergeSubcommands(primary: EnrichedCLINode[], secondary: EnrichedCLINode[]): EnrichedCLINode[] {
107
+ const byName = new Map<string, EnrichedCLINode>();
108
+
109
+ for (const sub of primary) byName.set(sub.name, sub);
110
+ for (const sub of secondary) {
111
+ const existing = byName.get(sub.name);
112
+ if (!existing) {
113
+ byName.set(sub.name, sub);
114
+ } else {
115
+ byName.set(sub.name, mergeNodes(existing, sub));
116
+ }
117
+ }
118
+
119
+ return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
120
+ }
121
+
122
+ function dedupeExamples(examples: { command: string; description?: string; source?: Source }[]) {
123
+ const byCommand = new Map<string, { command: string; description?: string; source?: Source }>();
124
+ for (const ex of examples) {
125
+ const existing = byCommand.get(ex.command);
126
+ if (!existing || sourceRank(ex.source) > sourceRank(existing.source)) {
127
+ byCommand.set(ex.command, ex);
128
+ }
129
+ }
130
+ return Array.from(byCommand.values());
131
+ }
132
+
133
+ export function filterUnverified(node: EnrichedCLINode): EnrichedCLINode {
134
+ const copy: EnrichedCLINode = { ...node };
135
+
136
+ if (copy.flags) {
137
+ copy.flags = copy.flags.filter(f => {
138
+ const rank = sourceRank(f.source);
139
+ return rank >= SOURCE_PRIORITY["llm-validated"];
140
+ });
141
+ if (copy.flags.length === 0) delete copy.flags;
142
+ }
143
+
144
+ if (copy.subcommands) {
145
+ copy.subcommands = copy.subcommands
146
+ .filter(s => {
147
+ const rank = sourceRank(s.source);
148
+ return rank >= SOURCE_PRIORITY["llm-validated"];
149
+ })
150
+ .map(filterUnverified);
151
+ if (copy.subcommands.length === 0) delete copy.subcommands;
152
+ }
153
+
154
+ return copy;
155
+ }
@@ -0,0 +1,185 @@
1
+ import { parseHelpRecursive } from "../parse";
2
+ import { validateSurvey, excavateCommand, validateDeepDive, summarizeValidation } from "./validate";
3
+ import { loadCache, saveCache, isCacheStale, detectCliVersion, createEmptyCache } from "./cache";
4
+ import { fromCLINode, toCLINode, type EnrichedCLINode, type ArchaeologyOptions, type ValidationVerdict, type Source } from "./types";
5
+ import type { ArchaeologyDelegate } from "./delegate";
6
+ import { NullDelegate } from "./delegate";
7
+ import type { CLINode } from "../types";
8
+
9
+ export interface ArchaeologyProgress {
10
+ phase: "cache" | "excavation" | "survey" | "deep-dive" | "cartography" | "merge" | "done";
11
+ message: string;
12
+ percent?: number;
13
+ }
14
+
15
+ export interface ArchaeologyResult {
16
+ tree: EnrichedCLINode;
17
+ stats: {
18
+ helpCommands: number;
19
+ llmProposed: number;
20
+ verified: number;
21
+ rejected: number;
22
+ unconfirmed: number;
23
+ hallucinations: string[];
24
+ };
25
+ fromCache: boolean;
26
+ }
27
+
28
+ export async function runArchaeology(
29
+ binary: string,
30
+ delegate: ArchaeologyDelegate = new NullDelegate(),
31
+ opts: ArchaeologyOptions & {
32
+ onProgress?: (p: ArchaeologyProgress) => void;
33
+ maxHelpDepth?: number;
34
+ } = {},
35
+ ): Promise<ArchaeologyResult> {
36
+ const report = opts.onProgress ?? (() => {});
37
+ const phases = {
38
+ survey: opts.phases?.survey ?? true,
39
+ excavation: opts.phases?.excavation ?? true,
40
+ deepDive: opts.phases?.deepDive ?? true,
41
+ cartography: opts.phases?.cartography ?? false,
42
+ };
43
+
44
+ report({ phase: "cache", message: "Checking cache..." });
45
+
46
+ const version = await detectCliVersion(binary);
47
+ if (!opts.skipCache) {
48
+ const cached = await loadCache(binary, { cacheDir: opts.cacheDir });
49
+ if (cached && !isCacheStale(cached, version, 30)) {
50
+ report({ phase: "done", message: "Loaded from cache" });
51
+ return {
52
+ tree: cached.tree,
53
+ stats: { helpCommands: 0, llmProposed: 0, verified: 0, rejected: 0, unconfirmed: 0, hallucinations: [] },
54
+ fromCache: true,
55
+ };
56
+ }
57
+ }
58
+
59
+ report({ phase: "excavation", message: `Parsing ${binary} --help recursively...` });
60
+ const helpTree = await parseHelpRecursive(binary, [], opts.maxHelpDepth ?? 2);
61
+ const helpSource: Source = { type: "help", confidence: 1.0, verifiedAt: new Date().toISOString() };
62
+ const tree: EnrichedCLINode = fromCLINode(helpTree, helpSource);
63
+ const helpCommandCount = countCommands(tree);
64
+
65
+ let llmProposed = 0;
66
+ const allVerdicts: ValidationVerdict[] = [];
67
+
68
+ if (delegate.available && phases.survey && delegate.survey) {
69
+ report({ phase: "survey", message: "Delegate hypothesis survey..." });
70
+ try {
71
+ const survey = await delegate.survey(binary, version);
72
+ const existingCommands = new Set(tree.subcommands?.map(s => s.name) ?? []);
73
+
74
+ const { confirmed } = await validateSurvey(binary, survey);
75
+ const llmSurveySource: Source = { type: "llm-validated", confidence: 0.8, verifiedAt: new Date().toISOString() };
76
+
77
+ for (const sub of confirmed) {
78
+ if (!existingCommands.has(sub)) {
79
+ llmProposed += 1;
80
+ const excavated = await excavateCommand(binary, [sub]);
81
+ if (excavated) {
82
+ const enriched = fromCLINode(excavated, llmSurveySource);
83
+ if (!tree.subcommands) tree.subcommands = [];
84
+ tree.subcommands.push(enriched);
85
+ }
86
+ }
87
+ }
88
+ } catch (e: any) {
89
+ report({ phase: "survey", message: `Survey failed: ${e.message}` });
90
+ }
91
+ }
92
+
93
+ if (delegate.available && phases.deepDive && delegate.proposeHiddenFlags && tree.subcommands) {
94
+ const topCommands = tree.subcommands.slice(0, 10);
95
+ report({ phase: "deep-dive", message: `Deep dive into ${topCommands.length} subcommands...` });
96
+
97
+ for (const sub of topCommands) {
98
+ try {
99
+ const proposal = await delegate.proposeHiddenFlags(binary, [sub.name]);
100
+ const verdict = await validateDeepDive(binary, proposal);
101
+ allVerdicts.push(verdict);
102
+
103
+ const verifiedSet = new Set(verdict.verifiedFlags);
104
+ const unconfirmedSet = new Set(verdict.unconfirmedFlags);
105
+
106
+ for (const flag of proposal.flags ?? []) {
107
+ if (verifiedSet.has(flag.name) || (opts.includeUnverified && unconfirmedSet.has(flag.name))) {
108
+ llmProposed += 1;
109
+ const source: Source = {
110
+ type: verifiedSet.has(flag.name) ? "llm-validated" : "llm-deep-dive",
111
+ confidence: verifiedSet.has(flag.name) ? 0.9 : 0.6,
112
+ verifiedAt: new Date().toISOString(),
113
+ };
114
+ if (!sub.flags) sub.flags = [];
115
+ const existing = sub.flags.find(f => f.name === flag.name);
116
+ if (!existing) {
117
+ sub.flags.push({
118
+ name: flag.name,
119
+ short: flag.short,
120
+ type: flag.type,
121
+ description: flag.description,
122
+ hidden: flag.hidden,
123
+ deprecated: flag.deprecated,
124
+ source,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ } catch (e: any) {
130
+ report({ phase: "deep-dive", message: `Deep dive failed for ${sub.name}: ${e.message}` });
131
+ }
132
+ }
133
+ }
134
+
135
+ if (delegate.available && phases.cartography && delegate.proposeConstraints && tree.subcommands) {
136
+ report({ phase: "cartography", message: "Mapping flag constraints..." });
137
+ for (const sub of tree.subcommands.slice(0, 5)) {
138
+ try {
139
+ const constraints = await delegate.proposeConstraints(binary, [sub.name]);
140
+ if (constraints.length > 0) {
141
+ sub.constraints = constraints;
142
+ }
143
+ } catch {}
144
+ }
145
+ }
146
+
147
+ report({ phase: "merge", message: "Finalizing tree..." });
148
+ const summary = summarizeValidation(allVerdicts);
149
+
150
+ if (!opts.skipCache) {
151
+ const cache = createEmptyCache(binary, tree, version);
152
+ cache.phases = {
153
+ survey: phases.survey,
154
+ excavation: true,
155
+ deepDive: phases.deepDive,
156
+ cartography: phases.cartography,
157
+ };
158
+ await saveCache(binary, cache, { cacheDir: opts.cacheDir });
159
+ }
160
+
161
+ report({ phase: "done", message: "Archaeology complete" });
162
+
163
+ return {
164
+ tree,
165
+ stats: {
166
+ helpCommands: helpCommandCount,
167
+ llmProposed,
168
+ verified: summary.verified,
169
+ rejected: summary.rejected,
170
+ unconfirmed: summary.unconfirmed,
171
+ hallucinations: summary.hallucinations,
172
+ },
173
+ fromCache: false,
174
+ };
175
+ }
176
+
177
+ function countCommands(node: EnrichedCLINode): number {
178
+ let count = 1;
179
+ if (node.subcommands) for (const s of node.subcommands) count += countCommands(s);
180
+ return count;
181
+ }
182
+
183
+ export function enrichedToTree(enriched: EnrichedCLINode): CLINode {
184
+ return toCLINode(enriched);
185
+ }