@llm4ts/shell 2.37.0 → 2.39.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/src/Cli.ts CHANGED
@@ -30,6 +30,12 @@ import {
30
30
  type WatchOptions
31
31
  } from "@llm4ts/runner/Watch"
32
32
  import { makeProfileProgram, type ProfileOptions } from "@llm4ts/runner/Profile"
33
+ import {
34
+ makeGraphProgram,
35
+ nodeGraphDependencies,
36
+ type GraphCommand,
37
+ type GraphFormat
38
+ } from "@llm4ts/runner/Graph"
33
39
  import { idleAfterFrom } from "@llm4ts/runner/AgentTree"
34
40
  import { describeExclusion } from "@llm4ts/flow/Roster"
35
41
  import { builtinKitsDir, discoverKits, kitTierPaths, type DiscoveredKit } from "@llm4ts/runner/Kits"
@@ -721,6 +727,182 @@ const rosterCommand = Command.make("roster", { repo: rosterRepo }, (config) =>
721
727
  Command.withSubcommands([rosterPauseCommand, rosterResumeCommand])
722
728
  )
723
729
 
730
+ // ---- graph (ADR 0030) -------------------------------------------------------
731
+
732
+ const graphFormats: ReadonlyArray<GraphFormat> = ["text", "json", "mermaid", "dot"]
733
+
734
+ type GraphSubcommand = "build" | "query" | "path" | "closure" | "stats" | "probe"
735
+
736
+ interface GraphFlags {
737
+ readonly repo: Option.Option<string>
738
+ readonly pack: Option.Option<string>
739
+ readonly format: string
740
+ readonly kind: Option.Option<string>
741
+ readonly hops: string
742
+ readonly max: string
743
+ readonly all: boolean
744
+ readonly force: boolean
745
+ readonly text?: string
746
+ readonly from?: string
747
+ readonly to?: string
748
+ readonly program?: string
749
+ }
750
+
751
+ const positiveInt = (name: string, raw: string): Effect.Effect<number, ShellUsageError> => {
752
+ const value = Number.parseInt(raw, 10)
753
+ return Number.isInteger(value) && value > 0
754
+ ? Effect.succeed(value)
755
+ : Effect.fail(
756
+ new ShellUsageError({ message: `${name} must be a positive integer, got '${raw}'` })
757
+ )
758
+ }
759
+
760
+ export const graphCommandFrom = (
761
+ sub: GraphSubcommand,
762
+ flags: GraphFlags,
763
+ cwd: string = process.cwd()
764
+ ): Effect.Effect<GraphCommand, ShellUsageError> =>
765
+ Effect.gen(function* () {
766
+ const repo = resolve(
767
+ cwd,
768
+ Option.getOrElse(flags.repo, () => ".")
769
+ )
770
+ const pack = Option.isSome(flags.pack) ? { pack: flags.pack.value } : {}
771
+ const format = graphFormats.find((candidate) => candidate === flags.format)
772
+ if (format === undefined) {
773
+ return yield* new ShellUsageError({
774
+ message: `format must be one of ${graphFormats.join(", ")}, got '${flags.format}'`
775
+ })
776
+ }
777
+ switch (sub) {
778
+ case "build":
779
+ return { _tag: "build", repo, ...pack }
780
+ case "stats":
781
+ return { _tag: "stats", repo, ...pack, format }
782
+ case "probe":
783
+ return { _tag: "probe", repo, ...pack, format }
784
+ case "query":
785
+ return {
786
+ _tag: "query",
787
+ repo,
788
+ ...pack,
789
+ text: flags.text ?? "",
790
+ ...(Option.isSome(flags.kind) ? { kind: flags.kind.value } : {}),
791
+ hops: yield* positiveInt("hops", flags.hops),
792
+ format,
793
+ all: flags.all,
794
+ force: flags.force
795
+ }
796
+ case "path":
797
+ return {
798
+ _tag: "path",
799
+ repo,
800
+ ...pack,
801
+ from: flags.from ?? "",
802
+ to: flags.to ?? "",
803
+ max: yield* positiveInt("max", flags.max),
804
+ format,
805
+ force: flags.force
806
+ }
807
+ case "closure":
808
+ return {
809
+ _tag: "closure",
810
+ repo,
811
+ ...pack,
812
+ program: flags.program ?? "",
813
+ max: yield* positiveInt("max", flags.max),
814
+ format,
815
+ force: flags.force
816
+ }
817
+ }
818
+ })
819
+
820
+ const graphFlags = {
821
+ repo: Flag.String("repo").pipe(
822
+ Flag.optional,
823
+ Flag.withDescription("The legacy repository (defaults to the current directory)")
824
+ ),
825
+ pack: Flag.String("pack").pipe(
826
+ Flag.optional,
827
+ Flag.withDescription("Pack name or directory (defaults to LLM4TS_PACK, then cobol-springboot)")
828
+ ),
829
+ format: Flag.String("format").pipe(
830
+ Flag.withDefault("text"),
831
+ Flag.withDescription("text | json | mermaid | dot")
832
+ ),
833
+ kind: Flag.String("kind").pipe(
834
+ Flag.optional,
835
+ Flag.withDescription("Only nodes of this kind (query)")
836
+ ),
837
+ hops: Flag.String("hops").pipe(
838
+ Flag.withDefault("1"),
839
+ Flag.withDescription("Neighbourhood radius (query)")
840
+ ),
841
+ max: Flag.String("max").pipe(
842
+ Flag.withDefault("12"),
843
+ Flag.withDescription("Max hops (path) or files (closure)")
844
+ ),
845
+ all: Flag.Boolean("all").pipe(
846
+ Flag.withDefault(false),
847
+ Flag.withDescription("Render the whole estate instead of the neighbourhood")
848
+ ),
849
+ force: Flag.Boolean("force").pipe(
850
+ Flag.withDefault(false),
851
+ Flag.withDescription("Render mermaid above the 150-node cap")
852
+ )
853
+ }
854
+
855
+ const runGraph = (sub: GraphSubcommand, flags: GraphFlags) =>
856
+ Effect.gen(function* () {
857
+ const command = yield* graphCommandFrom(sub, flags)
858
+ yield* Console.log(
859
+ yield* makeGraphProgram(command, nodeGraphDependencies(process.env, shellTierPaths()))
860
+ )
861
+ })
862
+
863
+ const graphBuildCommand = Command.make("build", graphFlags, (flags) =>
864
+ runGraph("build", flags)
865
+ ).pipe(
866
+ Command.withDescription(
867
+ "Scan the estate with the pack's rules and write .llm4ts/graph/<pack>.json"
868
+ )
869
+ )
870
+ const graphQueryCommand = Command.make(
871
+ "query",
872
+ { text: Argument.String("text"), ...graphFlags },
873
+ (flags) => runGraph("query", flags)
874
+ ).pipe(Command.withDescription("Nodes matching a text and their neighbourhood"))
875
+ const graphPathCommand = Command.make(
876
+ "path",
877
+ { from: Argument.String("from"), to: Argument.String("to"), ...graphFlags },
878
+ (flags) => runGraph("path", flags)
879
+ ).pipe(Command.withDescription("Shortest path between two node references, either direction"))
880
+ const graphClosureCommand = Command.make(
881
+ "closure",
882
+ { program: Argument.String("program"), ...graphFlags },
883
+ (flags) => runGraph("closure", flags)
884
+ ).pipe(Command.withDescription("A program's dependency closure as the analyst sees it"))
885
+ const graphStatsCommand = Command.make("stats", graphFlags, (flags) =>
886
+ runGraph("stats", flags)
887
+ ).pipe(Command.withDescription("Nodes, edges, mechanisms, origins and unresolved items by kind"))
888
+ const graphProbeCommand = Command.make("probe", graphFlags, (flags) =>
889
+ runGraph("probe", flags)
890
+ ).pipe(Command.withDescription("Evaluate the pack's '## Probe:' flows"))
891
+
892
+ const graphCommand = Command.make("graph", {}, () =>
893
+ Console.log("llm4ts graph <build|query|path|closure|stats|probe> --repo <legacy> [--pack <name>]")
894
+ ).pipe(
895
+ Command.withDescription("The legacy estate's code graph (ADR 0030): build it, query it, draw it"),
896
+ Command.withSubcommands([
897
+ graphBuildCommand,
898
+ graphQueryCommand,
899
+ graphPathCommand,
900
+ graphClosureCommand,
901
+ graphStatsCommand,
902
+ graphProbeCommand
903
+ ])
904
+ )
905
+
724
906
  const doctorCommand = Command.make("doctor", {}, () =>
725
907
  Effect.gen(function* () {
726
908
  const report = yield* makeDoctorProgram()
@@ -751,6 +933,7 @@ export const shellCommand = Command.make("llm4ts", {}, () =>
751
933
  costsCommand,
752
934
  watchCommand,
753
935
  profileCommand,
936
+ graphCommand,
754
937
  rosterCommand,
755
938
  doctorCommand
756
939
  ])