libretto 0.6.24 → 0.6.26

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 (63) hide show
  1. package/README.md +9 -1
  2. package/README.template.md +9 -1
  3. package/dist/cli/commands/browser.js +17 -10
  4. package/dist/cli/commands/cloud-credentials.js +70 -0
  5. package/dist/cli/commands/deploy.js +24 -2
  6. package/dist/cli/commands/execution.js +9 -30
  7. package/dist/cli/commands/import-chrome-profiles.js +46 -0
  8. package/dist/cli/commands/profiles.js +71 -0
  9. package/dist/cli/commands/shared.js +1 -3
  10. package/dist/cli/core/browser.js +89 -75
  11. package/dist/cli/core/daemon/daemon.js +47 -35
  12. package/dist/cli/core/daemon/ipc.js +3 -0
  13. package/dist/cli/core/deploy-artifact.js +85 -22
  14. package/dist/cli/core/profiles.js +47 -0
  15. package/dist/cli/core/prompt.js +9 -0
  16. package/dist/cli/core/providers/libretto-cloud.js +6 -2
  17. package/dist/cli/core/session-logs.js +325 -0
  18. package/dist/cli/core/telemetry.js +110 -311
  19. package/dist/cli/core/workflow-runner/runner.js +65 -0
  20. package/dist/cli/router.js +9 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +12 -0
  23. package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
  24. package/dist/shared/workflow/auth-profile-name.js +29 -0
  25. package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
  26. package/dist/shared/workflow/auth-profile-state.js +105 -0
  27. package/dist/shared/workflow/authenticate.d.ts +17 -0
  28. package/dist/shared/workflow/authenticate.js +37 -0
  29. package/dist/shared/workflow/credentials.d.ts +5 -0
  30. package/dist/shared/workflow/credentials.js +68 -0
  31. package/dist/shared/workflow/workflow.d.ts +16 -1
  32. package/dist/shared/workflow/workflow.js +56 -4
  33. package/package.json +1 -1
  34. package/skills/libretto/SKILL.md +3 -4
  35. package/skills/libretto/references/auth-profiles.md +61 -11
  36. package/skills/libretto/references/code-generation-rules.md +31 -1
  37. package/skills/libretto-readonly/SKILL.md +1 -1
  38. package/src/cli/commands/browser.ts +19 -11
  39. package/src/cli/commands/cloud-credentials.ts +82 -0
  40. package/src/cli/commands/deploy.ts +41 -2
  41. package/src/cli/commands/execution.ts +10 -31
  42. package/src/cli/commands/import-chrome-profiles.ts +46 -0
  43. package/src/cli/commands/profiles.ts +90 -0
  44. package/src/cli/commands/shared.ts +4 -8
  45. package/src/cli/core/browser.ts +102 -91
  46. package/src/cli/core/daemon/config.ts +4 -1
  47. package/src/cli/core/daemon/daemon.ts +52 -44
  48. package/src/cli/core/daemon/ipc.ts +15 -0
  49. package/src/cli/core/deploy-artifact.ts +131 -32
  50. package/src/cli/core/profiles.ts +53 -0
  51. package/src/cli/core/prompt.ts +15 -0
  52. package/src/cli/core/providers/libretto-cloud.ts +6 -2
  53. package/src/cli/core/providers/types.ts +4 -1
  54. package/src/cli/core/session-logs.ts +445 -0
  55. package/src/cli/core/telemetry.ts +142 -413
  56. package/src/cli/core/workflow-runner/runner.ts +86 -1
  57. package/src/cli/router.ts +8 -0
  58. package/src/index.ts +10 -0
  59. package/src/shared/workflow/auth-profile-name.ts +27 -0
  60. package/src/shared/workflow/auth-profile-state.ts +144 -0
  61. package/src/shared/workflow/authenticate.ts +63 -0
  62. package/src/shared/workflow/credentials.ts +91 -0
  63. package/src/shared/workflow/workflow.ts +89 -4
@@ -21,6 +21,7 @@ import {
21
21
  getWorkflowsFromModuleExports,
22
22
  LIBRETTO_WORKFLOW_BRAND,
23
23
  } from "../../shared/workflow/workflow.js";
24
+ import { normalizeCredentialNames } from "../../shared/workflow/credentials.js";
24
25
 
25
26
  type PackageManifest = {
26
27
  name?: string;
@@ -48,6 +49,14 @@ type HostedDeployPackage = {
48
49
  cleanup: () => void;
49
50
  entryPoint: string;
50
51
  outputDir: string;
52
+ workflows: WorkflowDeployMetadata[];
53
+ };
54
+
55
+ export type WorkflowDeployMetadata = {
56
+ name: string;
57
+ credentialNames: string[];
58
+ authProfileName?: string;
59
+ authProfileRefresh?: boolean;
51
60
  };
52
61
 
53
62
  type BuildHostedDeployTarballArgs = {
@@ -66,6 +75,7 @@ const DEFAULT_RUNTIME_EXTERNALS = [
66
75
  "chromium-bidi",
67
76
  ] as const;
68
77
  const BUILT_IN_MANIFEST_DEPENDENCIES = ["libretto"] as const;
78
+ const DEPLOY_METADATA_FILENAME = ".libretto-workflows.json";
69
79
  const SOURCE_FILE_EXTENSIONS = [
70
80
  "",
71
81
  ".ts",
@@ -598,6 +608,16 @@ function writeDeployManifest(args: {
598
608
  );
599
609
  }
600
610
 
611
+ function writeDeployMetadata(args: {
612
+ outputDir: string;
613
+ workflows: readonly WorkflowDeployMetadata[];
614
+ }): void {
615
+ writeFileSync(
616
+ join(args.outputDir, DEPLOY_METADATA_FILENAME),
617
+ JSON.stringify({ workflows: args.workflows }, null, 2) + "\n",
618
+ );
619
+ }
620
+
601
621
  function shouldVendorCurrentLibretto(versionSpec: string): boolean {
602
622
  return (
603
623
  versionSpec.startsWith("file:") ||
@@ -696,11 +716,17 @@ function createExternalDiscoveryStub(): object {
696
716
  });
697
717
  }
698
718
 
699
- function createDiscoveryLibrettoModule(workflowNames: Set<string>): object {
719
+ function createDiscoveryLibrettoModule(
720
+ workflowsByName: Map<string, WorkflowDeployMetadata>,
721
+ ): object {
700
722
  const moduleShape: Record<PropertyKey, unknown> = {
701
723
  LIBRETTO_WORKFLOW_BRAND,
702
- workflow: (name: string) => {
703
- workflowNames.add(name);
724
+ workflow: (name: string, definitionOrHandler?: unknown) => {
725
+ workflowsByName.set(name, {
726
+ name,
727
+ ...extractDiscoveryCredentialMetadata(definitionOrHandler),
728
+ ...extractDiscoveryAuthProfileMetadata(definitionOrHandler),
729
+ });
704
730
  return {
705
731
  [LIBRETTO_WORKFLOW_BRAND]: true,
706
732
  name,
@@ -724,19 +750,64 @@ function createDiscoveryLibrettoModule(workflowNames: Set<string>): object {
724
750
  });
725
751
  }
726
752
 
727
- function discoverBundledWorkflowNames(args: {
753
+ function extractDiscoveryCredentialMetadata(
754
+ definitionOrHandler: unknown,
755
+ ): Pick<WorkflowDeployMetadata, "credentialNames"> {
756
+ if (
757
+ !definitionOrHandler ||
758
+ typeof definitionOrHandler !== "object" ||
759
+ !("credentials" in definitionOrHandler)
760
+ ) {
761
+ return { credentialNames: [] };
762
+ }
763
+ const rawCredentials = (definitionOrHandler as { credentials?: unknown })
764
+ .credentials;
765
+ return {
766
+ credentialNames: Array.isArray(rawCredentials)
767
+ ? normalizeCredentialNames(rawCredentials)
768
+ : [],
769
+ };
770
+ }
771
+
772
+ function extractDiscoveryAuthProfileMetadata(
773
+ definitionOrHandler: unknown,
774
+ ): Omit<WorkflowDeployMetadata, "name" | "credentialNames"> {
775
+ if (
776
+ !definitionOrHandler ||
777
+ typeof definitionOrHandler !== "object" ||
778
+ !("authProfile" in definitionOrHandler)
779
+ ) {
780
+ return {};
781
+ }
782
+ const authProfile = (definitionOrHandler as { authProfile?: unknown }).authProfile;
783
+ if (typeof authProfile === "string") return { authProfileName: authProfile };
784
+ if (!authProfile || typeof authProfile !== "object") return {};
785
+ const record = authProfile as {
786
+ name?: unknown;
787
+ refresh?: unknown;
788
+ };
789
+ if (typeof record.name !== "string") return {};
790
+ return {
791
+ authProfileName: record.name,
792
+ ...(typeof record.refresh === "boolean"
793
+ ? { authProfileRefresh: record.refresh }
794
+ : {}),
795
+ };
796
+ }
797
+
798
+ function discoverBundledWorkflows(args: {
728
799
  absEntryPoint: string;
729
800
  absSourceDir: string;
730
801
  bundleBuffer: Buffer;
731
802
  externalPackages: ReadonlySet<string>;
732
- }): string[] {
803
+ }): WorkflowDeployMetadata[] {
733
804
  const discoveryPath = join(
734
805
  args.absSourceDir,
735
806
  `.libretto-deploy-discovery-${process.pid}-${Date.now()}.cjs`,
736
807
  );
737
808
  const originalRequire = Module.prototype.require;
738
- const workflowNames = new Set<string>();
739
- const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowNames);
809
+ const workflowsByName = new Map<string, WorkflowDeployMetadata>();
810
+ const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowsByName);
740
811
  let loadedModuleExports: Record<string, unknown> | null = null;
741
812
 
742
813
  try {
@@ -764,11 +835,11 @@ function discoverBundledWorkflowNames(args: {
764
835
  rmSync(discoveryPath, { force: true });
765
836
  }
766
837
 
767
- const discoveredWorkflowNames = [...workflowNames].sort((left, right) =>
768
- left.localeCompare(right),
838
+ const discoveredWorkflows = [...workflowsByName.values()].sort((left, right) =>
839
+ left.name.localeCompare(right.name),
769
840
  );
770
841
 
771
- if (discoveredWorkflowNames.length === 0) {
842
+ if (discoveredWorkflows.length === 0) {
772
843
  throw new Error(
773
844
  `No workflows were found in ${args.absEntryPoint}. Import the workflow files you want to deploy from the entry point, or export a workflow directly from it.`,
774
845
  );
@@ -779,23 +850,23 @@ function discoverBundledWorkflowNames(args: {
779
850
  (workflow) => workflow.name,
780
851
  ),
781
852
  );
782
- const nonExportedWorkflowNames = discoveredWorkflowNames.filter(
783
- (name) => !exportedWorkflowNames.has(name),
853
+ const nonExportedWorkflowNames = discoveredWorkflows.filter(
854
+ (workflow) => !exportedWorkflowNames.has(workflow.name),
784
855
  );
785
856
 
786
857
  if (nonExportedWorkflowNames.length > 0) {
787
858
  throw new Error(
788
- `Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.join(", ")}`,
859
+ `Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.map((workflow) => workflow.name).join(", ")}`,
789
860
  );
790
861
  }
791
862
 
792
- return discoveredWorkflowNames;
863
+ return discoveredWorkflows;
793
864
  }
794
865
 
795
866
  function createBootstrapSource(args: {
796
867
  bundleBuffer: Buffer;
797
868
  deploymentName: string;
798
- workflowNames: readonly string[];
869
+ workflows: readonly WorkflowDeployMetadata[];
799
870
  }): string {
800
871
  const bundleHash = createHash("sha256")
801
872
  .update(args.bundleBuffer)
@@ -805,10 +876,14 @@ function createBootstrapSource(args: {
805
876
  "base64",
806
877
  );
807
878
  const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
808
- const exportLines = args.workflowNames
879
+ const exportLines = args.workflows
809
880
  .map(
810
- (name, index) =>
811
- `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(name)});`,
881
+ (workflow, index) =>
882
+ `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(workflow.name)}, ${JSON.stringify({
883
+ credentialNames: workflow.credentialNames,
884
+ authProfileName: workflow.authProfileName,
885
+ authProfileRefresh: workflow.authProfileRefresh,
886
+ })});`,
812
887
  )
813
888
  .join("\n");
814
889
 
@@ -844,8 +919,8 @@ function ensureBundleFile() {
844
919
  return BUNDLE_FILENAME;
845
920
  }
846
921
 
847
- function createWorkflowProxy(workflowName) {
848
- return workflow(workflowName, async (ctx, input) => {
922
+ function createWorkflowProxy(workflowName, metadata) {
923
+ const handler = async (ctx, input) => {
849
924
  const impl = nativeRequire(ensureBundleFile());
850
925
  const target = getWorkflowFromModuleExports(impl, workflowName);
851
926
  if (!target || typeof target.run !== "function") {
@@ -854,6 +929,26 @@ function createWorkflowProxy(workflowName) {
854
929
  );
855
930
  }
856
931
  return await target.run(ctx, input);
932
+ };
933
+
934
+ if (!metadata?.authProfileName) {
935
+ return workflow(workflowName, {
936
+ credentials: Array.isArray(metadata?.credentialNames)
937
+ ? metadata.credentialNames
938
+ : [],
939
+ handler,
940
+ });
941
+ }
942
+
943
+ return workflow(workflowName, {
944
+ credentials: Array.isArray(metadata.credentialNames)
945
+ ? metadata.credentialNames
946
+ : [],
947
+ authProfile: {
948
+ name: metadata.authProfileName,
949
+ ...(typeof metadata.authProfileRefresh === "boolean" ? { refresh: metadata.authProfileRefresh } : {}),
950
+ },
951
+ handler,
857
952
  });
858
953
  }
859
954
 
@@ -868,7 +963,7 @@ async function writeBundledDeployEntrypoint(args: {
868
963
  externalPackages: ReadonlySet<string>;
869
964
  outputDir: string;
870
965
  workspacePackages: Map<string, WorkspacePackage>;
871
- }): Promise<void> {
966
+ }): Promise<WorkflowDeployMetadata[]> {
872
967
  try {
873
968
  // The implementation bundle is CommonJS so the bootstrap can load it lazily
874
969
  // with createRequire() after workflow discovery, while external packages
@@ -898,7 +993,7 @@ async function writeBundledDeployEntrypoint(args: {
898
993
  );
899
994
  }
900
995
 
901
- const workflowNames = discoverBundledWorkflowNames({
996
+ const workflows = discoverBundledWorkflows({
902
997
  absEntryPoint: args.absEntryPoint,
903
998
  absSourceDir: args.absSourceDir,
904
999
  bundleBuffer: Buffer.from(bundledImplementation.contents),
@@ -910,9 +1005,10 @@ async function writeBundledDeployEntrypoint(args: {
910
1005
  createBootstrapSource({
911
1006
  bundleBuffer: Buffer.from(bundledImplementation.contents),
912
1007
  deploymentName: args.deploymentName,
913
- workflowNames,
1008
+ workflows,
914
1009
  }),
915
1010
  );
1011
+ return workflows;
916
1012
  } catch (error) {
917
1013
  throw new Error(
918
1014
  `Failed to bundle deploy entry point ${args.absEntryPoint}.\n${formatBuildError(error)}`,
@@ -944,7 +1040,7 @@ export async function createHostedDeployPackage(
944
1040
  let callerOwnsTempRoot = false;
945
1041
 
946
1042
  try {
947
- await writeBundledDeployEntrypoint({
1043
+ const workflows = await writeBundledDeployEntrypoint({
948
1044
  absEntryPoint,
949
1045
  absSourceDir,
950
1046
  deploymentName: args.deploymentName,
@@ -967,17 +1063,19 @@ export async function createHostedDeployPackage(
967
1063
  outputDir,
968
1064
  sourceDir: absSourceDir,
969
1065
  });
1066
+ writeDeployMetadata({ outputDir, workflows });
970
1067
 
971
1068
  // Success transfers ownership of the temp directory to the caller, who is
972
1069
  // responsible for invoking cleanup() after the tarball/upload step.
973
1070
  callerOwnsTempRoot = true;
974
- return {
975
- cleanup: () => {
976
- rmSync(tempRoot, { force: true, recursive: true });
977
- },
978
- entryPoint: "index.js",
979
- outputDir,
980
- };
1071
+ return {
1072
+ cleanup: () => {
1073
+ rmSync(tempRoot, { force: true, recursive: true });
1074
+ },
1075
+ entryPoint: "index.js",
1076
+ outputDir,
1077
+ workflows,
1078
+ };
981
1079
  } finally {
982
1080
  // On any failure before we return, this function still owns the temp dir
983
1081
  // and must remove it to avoid leaking deploy workspaces in /tmp.
@@ -989,7 +1087,7 @@ export async function createHostedDeployPackage(
989
1087
 
990
1088
  export async function buildHostedDeployTarball(
991
1089
  args: BuildHostedDeployTarballArgs,
992
- ): Promise<{ entryPoint: string; source: string }> {
1090
+ ): Promise<{ entryPoint: string; source: string; workflows: WorkflowDeployMetadata[] }> {
993
1091
  const deployPackage = await createHostedDeployPackage(args);
994
1092
 
995
1093
  try {
@@ -1001,6 +1099,7 @@ export async function buildHostedDeployTarball(
1001
1099
  return {
1002
1100
  entryPoint: deployPackage.entryPoint,
1003
1101
  source: readFileSync(tarPath).toString("base64"),
1102
+ workflows: deployPackage.workflows,
1004
1103
  };
1005
1104
  } finally {
1006
1105
  deployPackage.cleanup();
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { PROFILES_DIR } from "./context.js";
5
+ import type { AuthProfileStorageState } from "../../shared/workflow/auth-profile-state.js";
6
+ import { normalizeProfileName } from "../../shared/workflow/auth-profile-name.js";
7
+
8
+ export { normalizeProfileName } from "../../shared/workflow/auth-profile-name.js";
9
+
10
+ export function getProfilePath(profileName: string): string {
11
+ return join(PROFILES_DIR, `${normalizeProfileName(profileName)}.json`);
12
+ }
13
+
14
+ export function hasProfile(profileName: string): boolean {
15
+ return existsSync(getProfilePath(profileName));
16
+ }
17
+
18
+ export function readProfile(profileName: string): AuthProfileStorageState {
19
+ const profilePath = getProfilePath(profileName);
20
+ const parsed = JSON.parse(readFileSync(profilePath, "utf8")) as unknown;
21
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
22
+ throw new Error(`Saved auth profile "${profileName}" is not a JSON object.`);
23
+ }
24
+ return parsed as AuthProfileStorageState;
25
+ }
26
+
27
+ export async function writeProfile(
28
+ profileName: string,
29
+ profile: AuthProfileStorageState,
30
+ ): Promise<string> {
31
+ const profilePath = getProfilePath(profileName);
32
+ await mkdir(dirname(profilePath), { recursive: true });
33
+ await writeFile(profilePath, JSON.stringify(profile, null, 2), "utf8");
34
+ return profilePath;
35
+ }
36
+
37
+ export function formatMissingLocalAuthProfileMessage(args: {
38
+ profileName: string;
39
+ profilePath: string;
40
+ session: string;
41
+ }): string {
42
+ return [
43
+ `Local auth profile not found: "${args.profileName}".`,
44
+ `Expected profile file: ${args.profilePath}`,
45
+ "To create it locally:",
46
+ ` 1. libretto open <site-url> --headed --session ${args.session}`,
47
+ " 2. Log in manually in the browser window.",
48
+ ` 3. libretto save ${args.profileName} --session ${args.session} --sites <site>`,
49
+ "Or import site-scoped state from Chrome with:",
50
+ ` libretto import-chrome-profiles ${args.profileName} --cdp-url <url> --sites <site>`,
51
+ "Local profile files are not uploaded to cloud profiles.",
52
+ ].join("\n");
53
+ }
@@ -26,6 +26,21 @@ export async function prompt(
26
26
  }
27
27
  }
28
28
 
29
+ export async function promptConfirm(
30
+ question: string,
31
+ opts: { defaultValue?: boolean } = {},
32
+ ): Promise<boolean> {
33
+ const defaultValue = opts.defaultValue ?? false;
34
+ if (!stdin.isTTY) return defaultValue;
35
+
36
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
37
+ const answer = (await prompt(`${question} ${suffix}`)).trim().toLowerCase();
38
+
39
+ if (answer.length === 0) return defaultValue;
40
+
41
+ return answer === "y" || answer === "yes";
42
+ }
43
+
29
44
  const CTRL_C = "";
30
45
  const CR = "\r";
31
46
  const LF = "\n";
@@ -23,7 +23,7 @@ export function createLibrettoCloudProvider(): ProviderApi {
23
23
  // The Libretto Cloud API is an oRPC RPCHandler, not plain REST, so inputs
24
24
  // must be wrapped as { json: ... } and outputs arrive the same way.
25
25
  return {
26
- async createSession() {
26
+ async createSession(options) {
27
27
  const browserSessionTimeoutSeconds = readPositiveNumberEnv(
28
28
  "LIBRETTO_TIMEOUT_SECONDS",
29
29
  DEFAULT_BROWSER_SESSION_TIMEOUT_SECONDS,
@@ -35,7 +35,11 @@ export function createLibrettoCloudProvider(): ProviderApi {
35
35
  "Content-Type": "application/json",
36
36
  },
37
37
  body: JSON.stringify({
38
- json: { timeout_seconds: browserSessionTimeoutSeconds },
38
+ json: {
39
+ timeout_seconds: browserSessionTimeoutSeconds,
40
+ profile_name: options?.authProfileName,
41
+ profile_persist: options?.authProfilePersist,
42
+ },
39
43
  }),
40
44
  });
41
45
  if (!resp.ok) {
@@ -18,6 +18,9 @@ export type ProviderCloseResult = {
18
18
  };
19
19
 
20
20
  export type ProviderApi = {
21
- createSession(): Promise<ProviderSession>;
21
+ createSession(options?: {
22
+ authProfileName?: string;
23
+ authProfilePersist?: boolean;
24
+ }): Promise<ProviderSession>;
22
25
  closeSession(sessionId: string): Promise<ProviderCloseResult>;
23
26
  };