@factorialco/fcode-cli 3.3.0 → 3.4.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/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @factorialco/fcode-cli
20
20
  $ fcode COMMAND
21
21
  running command...
22
22
  $ fcode (--version)
23
- @factorialco/fcode-cli/3.3.0 linux-x64 node-v22.22.3
23
+ @factorialco/fcode-cli/3.4.0 linux-x64 node-v22.22.3
24
24
  $ fcode --help [COMMAND]
25
25
  USAGE
26
26
  $ fcode COMMAND
@@ -161,6 +161,62 @@ export interface ProcessUiTrigger {
161
161
  */
162
162
  awaitResult?: boolean;
163
163
  }
164
+ export interface ProcessFactorial {
165
+ /** Master switch: nothing below is exposed while false. */
166
+ enabled: boolean;
167
+ /**
168
+ * Whether Factorial waits for the execution and receives its result. Defaults to true, and is
169
+ * omitted from metadata.json when it does.
170
+ */
171
+ awaitResult?: boolean;
172
+ /**
173
+ * Monolith policy keys the caller must hold: the outer list is an OR of alternatives, each
174
+ * inner list an AND. Free text as far as fcode is concerned. Omitted when empty.
175
+ */
176
+ requiredPolicies?: string[][];
177
+ uiTrigger?: ProcessFactorialUiTrigger;
178
+ form?: ProcessFactorialForm;
179
+ agentTool?: ProcessFactorialAgentTool;
180
+ backend?: ProcessFactorialBackend;
181
+ }
182
+ export interface ProcessFactorialUiTrigger {
183
+ enabled: boolean;
184
+ /** Identifier of the UI location the button is rendered in. Required while enabled. */
185
+ locationId?: string;
186
+ /** Button text; the only field of the block that accepts fcode.i18n("key") tokens. */
187
+ label?: string;
188
+ /** f0 icon name, e.g. "Globe". */
189
+ icon?: string;
190
+ }
191
+ export interface ProcessFactorialForm {
192
+ enabled: boolean;
193
+ /** Whether the form may be embedded anonymously. Omitted from metadata.json when false. */
194
+ public?: boolean;
195
+ /**
196
+ * Whether the app offers the form to its users from its own page in Factorial. The `factorial`
197
+ * block's name for `form.appRole: "USER_FACING_FORM"`. Omitted from metadata.json when false.
198
+ */
199
+ appTool?: boolean;
200
+ }
201
+ export type AgentToolEffect = 'READ' | 'WRITE' | 'DESTRUCTIVE';
202
+ export interface ProcessFactorialAgentTool {
203
+ enabled: boolean;
204
+ /** What the tool does, written for the model. */
205
+ description?: string;
206
+ effect?: AgentToolEffect;
207
+ whenToUse?: string;
208
+ whenNotToUse?: string[];
209
+ preconditions?: string[];
210
+ doesNotDo?: string[];
211
+ /** What the agent should do when the tool is unavailable or fails. */
212
+ degradation?: string;
213
+ /** Slug of the process this one previews the outcome of, when it is a dry run. */
214
+ simulatesFor?: string;
215
+ }
216
+ export interface ProcessFactorialBackend {
217
+ enabled: boolean;
218
+ }
219
+ export type LifecycleRole = 'INSTALL' | 'SETTINGS' | 'UNINSTALL' | 'SYNC';
164
220
  export interface ProcessMetadataFields {
165
221
  name: string;
166
222
  description?: string;
@@ -168,6 +224,7 @@ export interface ProcessMetadataFields {
168
224
  webhook?: ProcessWebhook;
169
225
  form?: ProcessForm;
170
226
  uiTrigger?: ProcessUiTrigger;
227
+ factorial?: ProcessFactorial;
171
228
  }
172
229
  export declare class Process extends Resource {
173
230
  id: string;
@@ -179,6 +236,9 @@ export declare class Process extends Resource {
179
236
  webhook?: ProcessWebhook;
180
237
  form?: ProcessForm;
181
238
  uiTrigger?: ProcessUiTrigger;
239
+ factorial?: ProcessFactorial;
240
+ /** Set by the platform for the reserved lifecycle slugs; not part of the content hash. */
241
+ lifecycleRole?: LifecycleRole;
182
242
  script: {
183
243
  parametersSchema: any;
184
244
  programmingLanguage: ProgrammingLanguage;
@@ -73,6 +73,7 @@ class Process extends Resource {
73
73
  this.webhook = metadata.webhook;
74
74
  this.form = metadata.form;
75
75
  this.uiTrigger = metadata.uiTrigger;
76
+ this.factorial = metadata.factorial;
76
77
  this.script = script;
77
78
  this.readme = readme;
78
79
  this.manifest = settings.dependencies.scopedToProcess && manifest !== null ? manifest : { dependencies: [] };
@@ -92,6 +93,7 @@ class Process extends Resource {
92
93
  // Inserted only when set, so every process that predates UI triggers keeps the
93
94
  // content hash already stored in .fcode (mirrors WorkspaceSettings.contentHash).
94
95
  ...(this.uiTrigger && { uiTrigger: this.uiTrigger }),
96
+ ...(this.factorial && { factorial: this.factorial }),
95
97
  manifestDependencies: this.manifest.dependencies,
96
98
  dependenciesScopedToProcess: this.settings.dependencies.scopedToProcess,
97
99
  ...(this.deletion && { deletion: deletionHashPart(this.deletion) }),
@@ -37,8 +37,23 @@ function buildProcessMetadata(process) {
37
37
  if (process.uiTrigger !== undefined) {
38
38
  metadata.uiTrigger = process.uiTrigger;
39
39
  }
40
+ if (process.factorial !== undefined) {
41
+ metadata.factorial = process.factorial;
42
+ }
40
43
  return metadata;
41
44
  }
45
+ /**
46
+ * The `factorial` block as the file has it, minus the `title` and `description` keys an earlier
47
+ * draft of the block carried: the process's own name and description took their place, so a file
48
+ * that still has them is read as if it did not, and the next save drops them.
49
+ */
50
+ function readFactorialBlock(raw) {
51
+ if (raw === undefined || raw === null || typeof raw !== 'object') {
52
+ return raw ?? undefined;
53
+ }
54
+ const { title: _title, description: _description, ...factorial } = raw;
55
+ return factorial;
56
+ }
42
57
  const PROCESS_SOURCE_CODE_FILE_NAME = (programmingLanguage) => (programmingLanguage === 'PYTHON' ? 'main.py' : 'index.js');
43
58
  exports.PROCESS_SOURCE_CODE_FILE_NAME = PROCESS_SOURCE_CODE_FILE_NAME;
44
59
  const PROCESS_DEPENDENCIES_FILE_NAME = (programmingLanguage) => PROGRAMMING_LANGUAGE_DEPENDENCIES_FILE_NAME[programmingLanguage];
@@ -163,6 +178,7 @@ class ProcessRepository extends resource_1.default {
163
178
  webhook: raw.webhook,
164
179
  form: raw.form,
165
180
  uiTrigger: raw.uiTrigger,
181
+ factorial: readFactorialBlock(raw.factorial),
166
182
  };
167
183
  }
168
184
  catch (error) {
@@ -67,7 +67,26 @@ declare class FcodeGraphQLClient {
67
67
  * using the default header — keeps the metadata.json it already had on disk.
68
68
  */
69
69
  private mapWebhook;
70
- mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, deletedAt, retainingVersionTags }: any): Process;
70
+ /**
71
+ * metadata.json omits every defaulted field of the `factorial` block, so the input restores
72
+ * them explicitly: every input field left out means "leave unchanged" on the platform, and the
73
+ * file is the source of truth. That includes the agent tool's clearable texts, sent as "" when
74
+ * the file has none so deleting the line clears the value. The platform reads a null or empty
75
+ * `uiTrigger.locationId` / `label` / `icon` and `agentTool.effect` as unchanged, though, so
76
+ * those are sent only when present (like the legacy `uiTrigger.label`).
77
+ */
78
+ private toFactorialInput;
79
+ /**
80
+ * The `factorial` block as metadata.json writes it: defaulted fields omitted, and a block or
81
+ * sub-block omitted only when disabled AND all defaults — a disabled sub-block that still carries
82
+ * settings (a tool contract, a trigger location, a public form) is kept, or a pull followed by a
83
+ * push would clear them on the platform. The platform always returns the block, so a process
84
+ * that never configured one comes back as all defaults and is mapped to no block at all, or every
85
+ * process in every workspace would gain a `factorial` key (and a new content hash) on its next
86
+ * pull.
87
+ */
88
+ private mapFactorialSettings;
89
+ mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, lifecycleRole, deletedAt, retainingVersionTags, }: any): Process;
71
90
  /**
72
91
  * GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
73
92
  * keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
@@ -40,6 +40,17 @@ const DELETION_FIELDS = `
40
40
  retainingVersionTags
41
41
  `;
42
42
  const DELETION_QUERY_FIELDS = ['includeDeleted', 'deletedAt', 'retainingVersionTags'];
43
+ // The optional fields of the agent tool contract, by kind. All are written to metadata.json only
44
+ // when present. On push a list is always sent and a clearable text always sent as at least "",
45
+ // since the platform clears both on an empty value; `effect` is the one text it does not clear,
46
+ // so that one is sent only when present.
47
+ const AGENT_TOOL_CLEARABLE_TEXT_FIELDS = ['description', 'whenToUse', 'degradation', 'simulatesFor'];
48
+ const AGENT_TOOL_TEXT_FIELDS = [...AGENT_TOOL_CLEARABLE_TEXT_FIELDS, 'effect'];
49
+ const AGENT_TOOL_LIST_FIELDS = ['whenNotToUse', 'preconditions', 'doesNotDo'];
50
+ /** A settings block that is disabled and holds nothing else is omitted from metadata.json. */
51
+ function unlessAllDefaults(block) {
52
+ return !block.enabled && Object.keys(block).length === 1 ? undefined : block;
53
+ }
43
54
  // Shared process selection so pull queries and push mutations return the same
44
55
  // fields, keeping mapProcess (and therefore contentHash) consistent.
45
56
  const PROCESS_FIELDS = `
@@ -49,6 +60,7 @@ const PROCESS_FIELDS = `
49
60
  description
50
61
  tags
51
62
  resolvingTeamSlug
63
+ lifecycleRole
52
64
  script {
53
65
  parametersSchema
54
66
  sourceCode
@@ -85,6 +97,36 @@ const PROCESS_FIELDS = `
85
97
  icon
86
98
  awaitResult
87
99
  }
100
+ factorial {
101
+ enabled
102
+ awaitResult
103
+ requiredPolicies
104
+ uiTrigger {
105
+ enabled
106
+ locationId
107
+ label
108
+ icon
109
+ }
110
+ form {
111
+ enabled
112
+ public
113
+ appTool
114
+ }
115
+ agentTool {
116
+ enabled
117
+ description
118
+ effect
119
+ whenToUse
120
+ whenNotToUse
121
+ preconditions
122
+ doesNotDo
123
+ degradation
124
+ simulatesFor
125
+ }
126
+ backend {
127
+ enabled
128
+ }
129
+ }
88
130
  dependencies {
89
131
  scopedToProcess
90
132
  }
@@ -468,16 +510,30 @@ class FcodeGraphQLClient {
468
510
  const settings = {
469
511
  dependencies: { scopedToProcess: process.settings.dependencies.scopedToProcess },
470
512
  };
513
+ // The platform mirrors `factorial` into the legacy `formSettings` / `uiTrigger` blocks, so
514
+ // once metadata.json carries it the legacy keys only contribute what the mirror leaves alone
515
+ // (the form's auth mode, and every app role but `USER_FACING_FORM`, which the block owns as
516
+ // `form.appTool`); their enablement is not sent, or the last one applied would win over the
517
+ // block the developer actually edits.
518
+ if (process.factorial) {
519
+ settings.factorial = this.toFactorialInput(process.factorial, process.form);
520
+ }
471
521
  if (process.form) {
472
- settings.formSettings = { enabled: process.form.enabled };
473
- if (process.form.appRole) {
474
- settings.formSettings.appRole = process.form.appRole;
522
+ const formSettings = process.factorial ? {} : { enabled: process.form.enabled };
523
+ // `USER_FACING_FORM` is the one app role the `factorial` block owns, as `form.appTool`, so
524
+ // once the block is there that value travels with it; the lifecycle roles, which the block
525
+ // has no switch for, keep coming from here.
526
+ if (process.form.appRole && !(process.factorial && process.form.appRole === 'USER_FACING_FORM')) {
527
+ formSettings.appRole = process.form.appRole;
475
528
  }
476
529
  if (process.form.authMode) {
477
- settings.formSettings.authMode = process.form.authMode;
530
+ formSettings.authMode = process.form.authMode;
531
+ }
532
+ if (Object.keys(formSettings).length > 0) {
533
+ settings.formSettings = formSettings;
478
534
  }
479
535
  }
480
- if (process.uiTrigger) {
536
+ if (process.uiTrigger && !process.factorial) {
481
537
  settings.uiTrigger = { enabled: process.uiTrigger.enabled };
482
538
  if (process.uiTrigger.locationId) {
483
539
  settings.uiTrigger.locationId = process.uiTrigger.locationId;
@@ -796,7 +852,112 @@ class FcodeGraphQLClient {
796
852
  }
797
853
  return webhook;
798
854
  }
799
- mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, deletedAt, retainingVersionTags }) {
855
+ /**
856
+ * metadata.json omits every defaulted field of the `factorial` block, so the input restores
857
+ * them explicitly: every input field left out means "leave unchanged" on the platform, and the
858
+ * file is the source of truth. That includes the agent tool's clearable texts, sent as "" when
859
+ * the file has none so deleting the line clears the value. The platform reads a null or empty
860
+ * `uiTrigger.locationId` / `label` / `icon` and `agentTool.effect` as unchanged, though, so
861
+ * those are sent only when present (like the legacy `uiTrigger.label`).
862
+ */
863
+ toFactorialInput(factorial, form) {
864
+ const input = {
865
+ enabled: factorial.enabled,
866
+ awaitResult: factorial.awaitResult ?? true,
867
+ requiredPolicies: factorial.requiredPolicies ?? [],
868
+ uiTrigger: { enabled: Boolean(factorial.uiTrigger?.enabled) },
869
+ form: {
870
+ enabled: Boolean(factorial.form?.enabled),
871
+ public: Boolean(factorial.form?.public),
872
+ // A file written before the flag existed says nothing about `appTool`, and the block wins
873
+ // over the legacy `appRole` applied before it — so an absent flag falls back to the role
874
+ // rather than to `false`, which would release it.
875
+ appTool: factorial.form?.appTool ?? form?.appRole === 'USER_FACING_FORM',
876
+ },
877
+ agentTool: {
878
+ enabled: Boolean(factorial.agentTool?.enabled),
879
+ whenNotToUse: factorial.agentTool?.whenNotToUse ?? [],
880
+ preconditions: factorial.agentTool?.preconditions ?? [],
881
+ doesNotDo: factorial.agentTool?.doesNotDo ?? [],
882
+ },
883
+ backend: { enabled: Boolean(factorial.backend?.enabled) },
884
+ };
885
+ for (const field of AGENT_TOOL_CLEARABLE_TEXT_FIELDS) {
886
+ input.agentTool[field] = factorial.agentTool?.[field] ?? '';
887
+ }
888
+ if (factorial.agentTool?.effect) {
889
+ input.agentTool.effect = factorial.agentTool.effect;
890
+ }
891
+ if (factorial.uiTrigger?.locationId) {
892
+ input.uiTrigger.locationId = factorial.uiTrigger.locationId;
893
+ }
894
+ if (factorial.uiTrigger?.label) {
895
+ input.uiTrigger.label = factorial.uiTrigger.label;
896
+ }
897
+ if (factorial.uiTrigger?.icon) {
898
+ input.uiTrigger.icon = factorial.uiTrigger.icon;
899
+ }
900
+ return input;
901
+ }
902
+ /**
903
+ * The `factorial` block as metadata.json writes it: defaulted fields omitted, and a block or
904
+ * sub-block omitted only when disabled AND all defaults — a disabled sub-block that still carries
905
+ * settings (a tool contract, a trigger location, a public form) is kept, or a pull followed by a
906
+ * push would clear them on the platform. The platform always returns the block, so a process
907
+ * that never configured one comes back as all defaults and is mapped to no block at all, or every
908
+ * process in every workspace would gain a `factorial` key (and a new content hash) on its next
909
+ * pull.
910
+ */
911
+ mapFactorialSettings(factorial) {
912
+ if (!factorial) {
913
+ return undefined;
914
+ }
915
+ const mapped = { enabled: Boolean(factorial.enabled) };
916
+ if (factorial.awaitResult === false) {
917
+ mapped.awaitResult = false;
918
+ }
919
+ if (Array.isArray(factorial.requiredPolicies) && factorial.requiredPolicies.length > 0) {
920
+ mapped.requiredPolicies = factorial.requiredPolicies;
921
+ }
922
+ if (factorial.uiTrigger) {
923
+ const uiTrigger = { enabled: Boolean(factorial.uiTrigger.enabled) };
924
+ for (const field of ['locationId', 'label', 'icon']) {
925
+ if (factorial.uiTrigger[field]) {
926
+ uiTrigger[field] = factorial.uiTrigger[field];
927
+ }
928
+ }
929
+ mapped.uiTrigger = unlessAllDefaults(uiTrigger);
930
+ }
931
+ if (factorial.form) {
932
+ const form = { enabled: Boolean(factorial.form.enabled) };
933
+ if (factorial.form.public) {
934
+ form.public = true;
935
+ }
936
+ if (factorial.form.appTool) {
937
+ form.appTool = true;
938
+ }
939
+ mapped.form = unlessAllDefaults(form);
940
+ }
941
+ if (factorial.agentTool) {
942
+ const agentTool = { enabled: Boolean(factorial.agentTool.enabled) };
943
+ for (const field of AGENT_TOOL_TEXT_FIELDS) {
944
+ if (factorial.agentTool[field]) {
945
+ agentTool[field] = factorial.agentTool[field];
946
+ }
947
+ }
948
+ for (const field of AGENT_TOOL_LIST_FIELDS) {
949
+ if (Array.isArray(factorial.agentTool[field]) && factorial.agentTool[field].length > 0) {
950
+ agentTool[field] = factorial.agentTool[field];
951
+ }
952
+ }
953
+ mapped.agentTool = unlessAllDefaults(agentTool);
954
+ }
955
+ if (factorial.backend?.enabled) {
956
+ mapped.backend = { enabled: true };
957
+ }
958
+ return unlessAllDefaults(_.omitBy(mapped, _.isUndefined));
959
+ }
960
+ mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, lifecycleRole, deletedAt, retainingVersionTags, }) {
800
961
  const dependencies = _.map(manifest?.dependencies, (dependency) => {
801
962
  return { name: dependency.name, specifier: dependency.gitUrl ?? dependency.semVer };
802
963
  });
@@ -832,7 +993,15 @@ class FcodeGraphQLClient {
832
993
  metadata.uiTrigger.awaitResult = true;
833
994
  }
834
995
  }
835
- return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags));
996
+ const factorial = this.mapFactorialSettings(settings?.factorial);
997
+ if (factorial) {
998
+ metadata.factorial = factorial;
999
+ }
1000
+ const process = new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags));
1001
+ if (lifecycleRole) {
1002
+ process.lifecycleRole = lifecycleRole;
1003
+ }
1004
+ return process;
836
1005
  }
837
1006
  /**
838
1007
  * GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
@@ -2516,5 +2516,5 @@
2516
2516
  ]
2517
2517
  }
2518
2518
  },
2519
- "version": "3.3.0"
2519
+ "version": "3.4.0"
2520
2520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/fcode-cli",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Factorial Code Command Line Interface",
6
6
  "homepage": "https://code.factorialhr.com/docs/cli/",