@granular-software/sdk 0.4.47 → 0.4.48

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/cli/index.js CHANGED
@@ -5434,7 +5434,9 @@ function resolveEndpointMode(explicitMode) {
5434
5434
  if (explicit === "local" || explicit === "production") {
5435
5435
  return explicit;
5436
5436
  }
5437
- const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
5437
+ const envMode = normalizeMode(
5438
+ readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV")
5439
+ );
5438
5440
  if (envMode === "local" || envMode === "production") {
5439
5441
  return envMode;
5440
5442
  }
@@ -7639,7 +7641,11 @@ function ensureBuiltinPermissionFiles() {
7639
7641
  for (const profile of getBuiltinPermissionProfiles()) {
7640
7642
  const filePath = path3__namespace.join(permissionsDir, `${profile.name}.json`);
7641
7643
  if (!fs__namespace.existsSync(filePath)) {
7642
- fs__namespace.writeFileSync(filePath, JSON.stringify(profile, null, 2) + "\n", "utf-8");
7644
+ fs__namespace.writeFileSync(
7645
+ filePath,
7646
+ JSON.stringify(profile, null, 2) + "\n",
7647
+ "utf-8"
7648
+ );
7643
7649
  }
7644
7650
  }
7645
7651
  }
@@ -7708,7 +7714,10 @@ function loadApiUrl() {
7708
7714
  if (process.env.GRANULAR_API_URL) return process.env.GRANULAR_API_URL;
7709
7715
  const modeOverride = process.env.GRANULAR_ENDPOINT_MODE;
7710
7716
  if (modeOverride === "local" || modeOverride === "production" || modeOverride === "prod") {
7711
- return resolveApiUrl(void 0, modeOverride === "prod" ? "production" : modeOverride);
7717
+ return resolveApiUrl(
7718
+ void 0,
7719
+ modeOverride === "prod" ? "production" : modeOverride
7720
+ );
7712
7721
  }
7713
7722
  const rc = readRcFile();
7714
7723
  if (rc.apiUrl) return rc.apiUrl;
@@ -7797,9 +7806,9 @@ var ApiClient = class {
7797
7806
  const response = await fetch(url, {
7798
7807
  ...options,
7799
7808
  headers: {
7800
- "Authorization": `Bearer ${this.apiKey}`,
7809
+ Authorization: `Bearer ${this.apiKey}`,
7801
7810
  "Content-Type": "application/json",
7802
- "Connection": "close",
7811
+ Connection: "close",
7803
7812
  ...options.headers
7804
7813
  }
7805
7814
  });
@@ -7908,10 +7917,13 @@ var ApiClient = class {
7908
7917
  }));
7909
7918
  }
7910
7919
  async moveTag(tagId, targetBuildId) {
7911
- const moved = await this.request(`/control/tags/${tagId}/move`, {
7912
- method: "POST",
7913
- body: JSON.stringify({ targetBuildId })
7914
- });
7920
+ const moved = await this.request(
7921
+ `/control/tags/${tagId}/move`,
7922
+ {
7923
+ method: "POST",
7924
+ body: JSON.stringify({ targetBuildId })
7925
+ }
7926
+ );
7915
7927
  return {
7916
7928
  ...moved,
7917
7929
  targetVersionId: moved.targetVersionId ?? moved.targetBuildId ?? null
@@ -7921,14 +7933,20 @@ var ApiClient = class {
7921
7933
  return this.moveTag(tagId, targetVersionId);
7922
7934
  }
7923
7935
  async promoteToProd(buildId) {
7924
- return this.request(`/control/builds/${buildId}/promote`, {
7925
- method: "POST"
7926
- });
7936
+ return this.request(
7937
+ `/control/builds/${buildId}/promote`,
7938
+ {
7939
+ method: "POST"
7940
+ }
7941
+ );
7927
7942
  }
7928
7943
  async promoteVersionToProd(versionId) {
7929
- return this.request(`/control/versions/${versionId}/promote`, {
7930
- method: "POST"
7931
- });
7944
+ return this.request(
7945
+ `/control/versions/${versionId}/promote`,
7946
+ {
7947
+ method: "POST"
7948
+ }
7949
+ );
7932
7950
  }
7933
7951
  async getBuildDiff(buildId, againstBuildId) {
7934
7952
  const query = againstBuildId ? `?against=${encodeURIComponent(againstBuildId)}` : "";
@@ -7969,10 +7987,14 @@ var ApiClient = class {
7969
7987
  schemaVersion: 1,
7970
7988
  name: data.name
7971
7989
  };
7972
- const result = await this.syncPermissionProfileSources(sandboxId, [profile]);
7990
+ const result = await this.syncPermissionProfileSources(sandboxId, [
7991
+ profile
7992
+ ]);
7973
7993
  const synced = result.items?.find((item) => item.name === data.name) || result.items?.[0];
7974
7994
  if (!synced) {
7975
- throw new Error(`Permission profile source sync did not return ${data.name}`);
7995
+ throw new Error(
7996
+ `Permission profile source sync did not return ${data.name}`
7997
+ );
7976
7998
  }
7977
7999
  return synced;
7978
8000
  }
@@ -7989,22 +8011,19 @@ var ApiClient = class {
7989
8011
  });
7990
8012
  }
7991
8013
  async syncPermissionProfileSources(sandboxId, profiles) {
7992
- return this.request(
7993
- `/control/sandboxes/${sandboxId}/permission-profile-sources`,
7994
- {
7995
- method: "PUT",
7996
- body: JSON.stringify({ profiles })
7997
- }
7998
- );
8014
+ return this.request(`/control/sandboxes/${sandboxId}/permission-profile-sources`, {
8015
+ method: "PUT",
8016
+ body: JSON.stringify({ profiles })
8017
+ });
7999
8018
  }
8000
8019
  async getVersionPolicies(versionId) {
8001
- return this.request(`/control/ontology-versions/${versionId}/policies`);
8002
- }
8003
- async getVersionPermissionProfiles(versionId) {
8004
8020
  return this.request(
8005
- `/control/ontology-versions/${versionId}/permission-profiles`
8021
+ `/control/ontology-versions/${versionId}/policies`
8006
8022
  );
8007
8023
  }
8024
+ async getVersionPermissionProfiles(versionId) {
8025
+ return this.request(`/control/ontology-versions/${versionId}/permission-profiles`);
8026
+ }
8008
8027
  async evaluateVersionPolicy(versionId, request) {
8009
8028
  return this.request(`/control/builds/${versionId}/policies/evaluate`, {
8010
8029
  method: "POST",
@@ -8094,7 +8113,9 @@ function generateHilbertPath(order = 4, size = 128, radius = 51) {
8094
8113
  const [sqX, sqY] = squareToSquircle(normX, normY);
8095
8114
  points.push([center + sqX * radius, center + sqY * radius]);
8096
8115
  }
8097
- return points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x.toFixed(3)} ${y.toFixed(3)}`).join(" ");
8116
+ return points.map(
8117
+ ([x, y], i) => `${i === 0 ? "M" : "L"} ${x.toFixed(3)} ${y.toFixed(3)}`
8118
+ ).join(" ");
8098
8119
  }
8099
8120
  function renderLogoSvg() {
8100
8121
  const squirclePath = generateSquirclePath();
@@ -8294,7 +8315,11 @@ async function loginWithBrowser(options) {
8294
8315
  const timeout = setTimeout(() => {
8295
8316
  if (settled) return;
8296
8317
  settled = true;
8297
- reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for browser authentication.`));
8318
+ reject(
8319
+ new Error(
8320
+ `Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for browser authentication.`
8321
+ )
8322
+ );
8298
8323
  }, timeoutMs);
8299
8324
  server.on("request", (req, res) => {
8300
8325
  const url = new URL(req.url || "/", `http://${CALLBACK_HOST}`);
@@ -8309,7 +8334,13 @@ async function loginWithBrowser(options) {
8309
8334
  if (!returnedState || returnedState !== state) {
8310
8335
  res.statusCode = 400;
8311
8336
  res.setHeader("Content-Type", "text/html; charset=utf-8");
8312
- res.end(renderHtmlPage("Granular login failed", "State mismatch. Please close this window and retry.", "error"));
8337
+ res.end(
8338
+ renderHtmlPage(
8339
+ "Granular login failed",
8340
+ "State mismatch. Please close this window and retry.",
8341
+ "error"
8342
+ )
8343
+ );
8313
8344
  if (!settled) {
8314
8345
  settled = true;
8315
8346
  clearTimeout(timeout);
@@ -8337,7 +8368,13 @@ async function loginWithBrowser(options) {
8337
8368
  if (!apiKey2) {
8338
8369
  res.statusCode = 400;
8339
8370
  res.setHeader("Content-Type", "text/html; charset=utf-8");
8340
- res.end(renderHtmlPage("Granular login failed", "No API key was returned. Please retry.", "error"));
8371
+ res.end(
8372
+ renderHtmlPage(
8373
+ "Granular login failed",
8374
+ "No API key was returned. Please retry.",
8375
+ "error"
8376
+ )
8377
+ );
8341
8378
  if (!settled) {
8342
8379
  settled = true;
8343
8380
  clearTimeout(timeout);
@@ -8347,7 +8384,13 @@ async function loginWithBrowser(options) {
8347
8384
  }
8348
8385
  res.statusCode = 200;
8349
8386
  res.setHeader("Content-Type", "text/html; charset=utf-8");
8350
- res.end(renderHtmlPage("Granular login complete", "Authentication succeeded. You can close this window.", "success"));
8387
+ res.end(
8388
+ renderHtmlPage(
8389
+ "Granular login complete",
8390
+ "Authentication succeeded. You can close this window.",
8391
+ "success"
8392
+ )
8393
+ );
8351
8394
  if (!settled) {
8352
8395
  settled = true;
8353
8396
  clearTimeout(timeout);
@@ -9741,7 +9784,9 @@ function table(headers, rows) {
9741
9784
  }
9742
9785
  }
9743
9786
  function hint(command, description) {
9744
- console.log(` ${brand.muted("$")} ${brand.secondary(command)} ${brand.muted(description)}`);
9787
+ console.log(
9788
+ ` ${brand.muted("$")} ${brand.secondary(command)} ${brand.muted(description)}`
9789
+ );
9745
9790
  }
9746
9791
  function nextSteps(steps) {
9747
9792
  console.log();
@@ -9875,7 +9920,7 @@ The rest of this guide assumes these terms.
9875
9920
  | **Build run** | The CI-style compilation process. \`granular build\` creates or reuses the ontology version, then runs a build for it. |
9876
9921
  | **Environment** | Return value of \`granular.openEnvironment({ ontology, tag, userId, \u2026 })\`. It is your **server-side** handle for \`recordObject\`, \`recordObjects\`, \`graphql\`, and \`sessions.create()\`. |
9877
9922
  | **Session** | Live runtime connection opened from an environment with \`environment.sessions.create()\` or \`environment.sessions.connect()\`. Job, prompt, and streaming APIs live here. |
9878
- | **Job** | Code string passed to \`session.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to \`./sandbox-tools\` (generated classes + effect entrypoints). |
9923
+ | **Job** | Code string passed to \`session.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to Harness v3 imports such as \`@granular/domain/<Class>\` and \`@granular/actions/backend\`. |
9879
9924
  | **Effect** | Declared with \`withEffect\`; **handler** registered in **your** process with \`granular.ontology(sandboxId).effects.registerMany(...)\`. Jobs invoke effects; handlers do IO outside Granular. |
9880
9925
  | **Class** | Entity **kind** in the domain (e.g. \`book\`) with \`has\` fields in the manifest. |
9881
9926
  | **Record / object** | One **instance**: \`className\`, \`id\`, \`fields\`, \`relationships\`. Use \`recordObject\` for one upsert, \`recordObjects\` for several immediate upserts, and record-import APIs for async bulk loads. |
@@ -9900,7 +9945,7 @@ function manifestGuideEndToEndSection() {
9900
9945
  | 5 | \`recordObject\` / \`recordObjects\` | **Records** stored with correct fields and relationship keys. Use \`recordObject\` for one targeted write. Use \`recordObjects\` for one synchronous multi-record write with chunking (default 100 rows/request), retries, and optional \`onChunkComplete\` / \`concurrency\` progress. For async bulk loads use \`enqueueRecordImport\` + import status APIs. |
9901
9946
  | 6 | \`const session = await environment.sessions.create();\` then \`session.submitJob(\`\u2026\`)\` | **Jobs** run; may call **effects** \u2192 your handlers execute and return results |
9902
9947
 
9903
- **Accuracy tip:** After changing the manifest, always **re-build** before assuming generated types, \`./sandbox-tools\` names, or effect signatures match the file on disk.`;
9948
+ **Accuracy tip:** After changing the manifest, always **re-build** before assuming generated Harness v3 imports, generated types, or effect signatures match the file on disk.`;
9904
9949
  }
9905
9950
  function sandboxDocMainConceptsSection() {
9906
9951
  return `## Main concepts (recap)
@@ -9914,7 +9959,7 @@ function sandboxDocMainConceptsSection() {
9914
9959
  | **Build run** | Compilation run that validates and materializes a version. |
9915
9960
  | **Environment** | \`openEnvironment()\` result \u2014 **record**, **recordObjects**, **graphql**, and **session lifecycle** for this sandbox. |
9916
9961
  | **Session** | Live runtime connection created from an environment; job and prompt APIs live here. |
9917
- | **Job** | \`session.submitJob(...)\` code using \`./sandbox-tools\`. |
9962
+ | **Job** | \`session.submitJob(...)\` code using Harness v3 runtime imports. |
9918
9963
  | **Effect** | Declared in manifest; **handler** in your process. |
9919
9964
 
9920
9965
  **Full product + manifest how-to:** [docs/granular-manifest.md](docs/granular-manifest.md).`;
@@ -14126,6 +14171,9 @@ external_exports.object({
14126
14171
  mode: external_exports.string().optional()
14127
14172
  }).strict()
14128
14173
  ]).optional(),
14174
+ access: external_exports.enum(["read", "write", "ui"]).optional(),
14175
+ effectKind: external_exports.enum(["read", "write", "ui"]).optional(),
14176
+ sideEffect: external_exports.enum(["read", "write", "ui", "readonly", "read_only"]).optional(),
14129
14177
  policies: PoliciesSchema.optional()
14130
14178
  }).strict();
14131
14179
 
@@ -14585,7 +14633,12 @@ var effectBehaviorsMetamodelPackage = defineMetamodelPackage({
14585
14633
  description
14586
14634
  })
14587
14635
  );
14588
- return { model, kind: "dry_run", enabled: finalEnabled, description };
14636
+ return {
14637
+ model,
14638
+ kind: "dry_run",
14639
+ enabled: finalEnabled,
14640
+ description
14641
+ };
14589
14642
  },
14590
14643
  set_reverse: async (ant, { handler, description }) => {
14591
14644
  const model = await run(
@@ -14631,7 +14684,10 @@ var effectBehaviorsMetamodelPackage = defineMetamodelPackage({
14631
14684
  applyToMethodIR(methodIR, methodSummary) {
14632
14685
  return {
14633
14686
  ...methodIR,
14634
- docs: [...methodIR.docs, ...buildEffectBehaviorDocs(methodSummary.effectBehaviors)]
14687
+ docs: [
14688
+ ...methodIR.docs,
14689
+ ...buildEffectBehaviorDocs(methodSummary.effectBehaviors)
14690
+ ]
14635
14691
  };
14636
14692
  }
14637
14693
  }
@@ -14895,7 +14951,9 @@ var filterByMetamodelPackage = defineMetamodelPackage({
14895
14951
 
14896
14952
  // ../metamodel-note/src/index.ts
14897
14953
  function noteTexts(values) {
14898
- return (values || []).map((item) => item?.text).filter((value) => typeof value === "string" && value.length > 0);
14954
+ return (values || []).map((item) => item?.text).filter(
14955
+ (value) => typeof value === "string" && value.length > 0
14956
+ );
14899
14957
  }
14900
14958
  function buildNoteMutations(targetPath, notes) {
14901
14959
  return normalizeNotesInput(notes).map((note) => ({
@@ -14925,7 +14983,10 @@ var noteMetamodelPackage = defineMetamodelPackage({
14925
14983
  id: "note",
14926
14984
  docs: {
14927
14985
  fieldRows: [
14928
- { key: "note", description: "Advisory text attached to a field. Accepts a string or string array." }
14986
+ {
14987
+ key: "note",
14988
+ description: "Advisory text attached to a field. Accepts a string or string array."
14989
+ }
14929
14990
  ],
14930
14991
  modelRows: [
14931
14992
  { key: "note", description: "Advisory text on the class/model itself." }
@@ -15065,7 +15126,9 @@ function buildRequiredFieldMutations(fieldPath, required) {
15065
15126
  var requiredMetamodelPackage = defineMetamodelPackage({
15066
15127
  id: "required",
15067
15128
  docs: {
15068
- fieldRows: [{ key: "required", description: "Marks the field as required." }]
15129
+ fieldRows: [
15130
+ { key: "required", description: "Marks the field as required." }
15131
+ ]
15069
15132
  },
15070
15133
  graphql: {
15071
15134
  typeDefs: [
@@ -15123,7 +15186,10 @@ var requiredMetamodelPackage = defineMetamodelPackage({
15123
15186
  if (!propertySummary.required) return propertyIR;
15124
15187
  return {
15125
15188
  ...propertyIR,
15126
- docs: [...propertyIR.docs, propertySummary.required.message || "Required."]
15189
+ docs: [
15190
+ ...propertyIR.docs,
15191
+ propertySummary.required.message || "Required."
15192
+ ]
15127
15193
  };
15128
15194
  }
15129
15195
  }
@@ -15274,7 +15340,10 @@ function normalizeStateDefinitions(machine) {
15274
15340
  const states = /* @__PURE__ */ new Map();
15275
15341
  for (const rawState of machine.states || []) {
15276
15342
  if (typeof rawState === "string") {
15277
- states.set(rawState, { name: rawState, isFinal: finalStates.has(rawState) });
15343
+ states.set(rawState, {
15344
+ name: rawState,
15345
+ isFinal: finalStates.has(rawState)
15346
+ });
15278
15347
  continue;
15279
15348
  }
15280
15349
  states.set(rawState.name, {
@@ -15362,7 +15431,9 @@ function buildMachineMethods(classSummary, machine) {
15362
15431
  },
15363
15432
  {
15364
15433
  name: `reach_${machine.name}`,
15365
- docs: [`Reach a ${docsPrefix} state through the shortest allowed transition path.`],
15434
+ docs: [
15435
+ `Reach a ${docsPrefix} state through the shortest allowed transition path.`
15436
+ ],
15366
15437
  static: false,
15367
15438
  params: [{ name: "target", type: stateName }],
15368
15439
  returnType: `Promise<${toPascalCase2(classSummary.name)}>`,
@@ -15422,7 +15493,9 @@ function buildMachineMethods(classSummary, machine) {
15422
15493
  },
15423
15494
  {
15424
15495
  name: `paths_to_${machine.name}`,
15425
- docs: [`List shortest transition paths from the current ${docsPrefix} state to a target state.`],
15496
+ docs: [
15497
+ `List shortest transition paths from the current ${docsPrefix} state to a target state.`
15498
+ ],
15426
15499
  static: false,
15427
15500
  params: [{ name: "target", type: stateName }],
15428
15501
  returnType: `Promise<Array<{ states: ${stateName}[]; transitions: ${transitionName}[] }>>`,
@@ -15555,22 +15628,39 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15555
15628
  name: (value) => value.name,
15556
15629
  state_machine: async (value) => await run(value.target.state_machine(value.name)),
15557
15630
  add_state: async (value, { name, is_final }) => {
15558
- await run(value.target.add_state_machine_state(value.name, name, is_final ?? false));
15631
+ await run(
15632
+ value.target.add_state_machine_state(
15633
+ value.name,
15634
+ name,
15635
+ is_final ?? false
15636
+ )
15637
+ );
15559
15638
  return value;
15560
15639
  },
15561
15640
  add_transition: async (value, { name, from, to }) => {
15562
- await run(value.target.add_state_machine_transition(value.name, name, from, to));
15641
+ await run(
15642
+ value.target.add_state_machine_transition(
15643
+ value.name,
15644
+ name,
15645
+ from,
15646
+ to
15647
+ )
15648
+ );
15563
15649
  return value;
15564
15650
  },
15565
15651
  activate_transition: async (value, { name }) => {
15566
- await run(value.target.activate_state_machine_transition(value.name, name));
15652
+ await run(
15653
+ value.target.activate_state_machine_transition(value.name, name)
15654
+ );
15567
15655
  return value;
15568
15656
  }
15569
15657
  },
15570
15658
  StateMachineSnapshotMutation: {
15571
15659
  snapshot: async (value) => await run(value.target.state_machine(value.name)),
15572
15660
  activate_transition: async (value, { name }) => {
15573
- await run(value.target.activate_state_machine_transition(value.name, name));
15661
+ await run(
15662
+ value.target.activate_state_machine_transition(value.name, name)
15663
+ );
15574
15664
  return value;
15575
15665
  }
15576
15666
  },
@@ -15601,7 +15691,11 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15601
15691
  reachable_states: (value) => value.reachable_states,
15602
15692
  is_final: (value) => value.is_final,
15603
15693
  history: (value) => value.history,
15604
- paths_to: async (value, { state }) => await stateMachines.pathsToState(value.model.target || value.model, value.name, state)
15694
+ paths_to: async (value, { state }) => await stateMachines.pathsToState(
15695
+ value.model.target || value.model,
15696
+ value.name,
15697
+ state
15698
+ )
15605
15699
  },
15606
15700
  StateMachine: {
15607
15701
  name: (value) => value.name,
@@ -15614,8 +15708,16 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15614
15708
  reachable_states: (value) => value.reachable_states,
15615
15709
  is_final: (value) => value.is_final,
15616
15710
  history: (value) => value.history,
15617
- paths_to: async (value, { state }) => await stateMachines.pathsToState(value.model.target || value.model, value.name, state),
15618
- instances_in_state: async (value, { state }) => await stateMachines.instancesInState(value.model.target || value.model, value.name, state)
15711
+ paths_to: async (value, { state }) => await stateMachines.pathsToState(
15712
+ value.model.target || value.model,
15713
+ value.name,
15714
+ state
15715
+ ),
15716
+ instances_in_state: async (value, { state }) => await stateMachines.instancesInState(
15717
+ value.model.target || value.model,
15718
+ value.name,
15719
+ state
15720
+ )
15619
15721
  }
15620
15722
  };
15621
15723
  }
@@ -15659,9 +15761,12 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15659
15761
  // ../metamodel-validation-rule/src/index.ts
15660
15762
  function describeRule(rule) {
15661
15763
  if (rule.message) return rule.message;
15662
- if (rule.stringValue !== void 0) return `${rule.operator} ${JSON.stringify(rule.stringValue)}`;
15663
- if (rule.numberValue !== void 0) return `${rule.operator} ${rule.numberValue}`;
15664
- if (rule.booleanValue !== void 0) return `${rule.operator} ${String(rule.booleanValue)}`;
15764
+ if (rule.stringValue !== void 0)
15765
+ return `${rule.operator} ${JSON.stringify(rule.stringValue)}`;
15766
+ if (rule.numberValue !== void 0)
15767
+ return `${rule.operator} ${rule.numberValue}`;
15768
+ if (rule.booleanValue !== void 0)
15769
+ return `${rule.operator} ${String(rule.booleanValue)}`;
15665
15770
  return rule.operator;
15666
15771
  }
15667
15772
  function normalizeRule(rule) {
@@ -15787,10 +15892,14 @@ var validationRuleMetamodelPackage = defineMetamodelPackage({
15787
15892
  },
15788
15893
  summary: {
15789
15894
  selections: {
15790
- propertyFields: [`validation_rules { operator string_value number_value boolean_value message }`]
15895
+ propertyFields: [
15896
+ `validation_rules { operator string_value number_value boolean_value message }`
15897
+ ]
15791
15898
  },
15792
15899
  readPropertySummary(rawProperty) {
15793
- const rules = Array.isArray(rawProperty.validation_rules) ? rawProperty.validation_rules.map(normalizeRule).filter((rule) => Boolean(rule)) : [];
15900
+ const rules = Array.isArray(rawProperty.validation_rules) ? rawProperty.validation_rules.map(normalizeRule).filter(
15901
+ (rule) => Boolean(rule)
15902
+ ) : [];
15794
15903
  return {
15795
15904
  validationRules: rules
15796
15905
  };
@@ -16323,7 +16432,7 @@ ${effectMetamodelTable}
16323
16432
 
16324
16433
  | API | Purpose |
16325
16434
  |-----|---------|
16326
- | \`submitJob(code)\` | Run code in the sandbox; import from \`./sandbox-tools\`. |
16435
+ | \`submitJob(code)\` | Run code in the sandbox; import generated classes from \`@granular/domain/<Class>\` and actions from \`@granular/actions/backend\` or \`@granular/actions/frontend\`. |
16327
16436
  | \`answerPrompt(...)\`, \`appendMessage(...)\` | Human-in-the-loop and conversation APIs. |
16328
16437
  | \`getDomain()\`, \`getDomainTypes()\`, \`getDomainDocs()\`, \`getDomainDocumentation()\` | Domain summary and generated TypeScript / docs. |
16329
16438
  | \`getEffects()\` / \`getTools()\`, \`session.on("effects:changed", ...)\` | Effect catalog and live updates. |
@@ -16656,7 +16765,7 @@ function generateSandboxAgentDoc(manifest, meta) {
16656
16765
  "2. **Register** a handler whose `name` (and `className` / `static` when applicable) matches \u2014 use `granular.ontology('" + meta.sandboxId + "').effects.registerMany([...])` in your host process (see `granular-effects.ts` in starter projects)."
16657
16766
  );
16658
16767
  lines.push(
16659
- "3. **Invoke** from sandbox code via `./sandbox-tools` inside `submitJob`; the handler you registered runs in your process and the result returns to the job."
16768
+ "3. **Invoke** from sandbox code via Harness v3 runtime imports inside `submitJob`; the handler you registered runs in your process and the result returns to the job."
16660
16769
  );
16661
16770
  lines.push("");
16662
16771
  lines.push(
@@ -16713,7 +16822,7 @@ function generateSandboxAgentDoc(manifest, meta) {
16713
16822
  lines.push("## `submitJob` / sandbox code");
16714
16823
  lines.push("");
16715
16824
  lines.push(
16716
- "Jobs run in the sandbox with generated classes from `./sandbox-tools`. Class names are PascalCase."
16825
+ "Jobs run in the sandbox with generated classes from `@granular/domain/<Class>` and global backend actions from `@granular/actions/backend`. Class names are PascalCase."
16717
16826
  );
16718
16827
  lines.push("");
16719
16828
  const pascalImports = classes.map((c) => toPascalCase(c.name));
@@ -16723,13 +16832,20 @@ function generateSandboxAgentDoc(manifest, meta) {
16723
16832
  lines.push("```typescript");
16724
16833
  lines.push(`const session = await env.sessions.create();
16725
16834
  const job = await session.submitJob(\``);
16726
- const importList = [...pascalImports, ...effectNames].filter(
16727
- (v, i, a) => a.indexOf(v) === i
16728
- );
16729
- if (importList.length > 0) {
16730
- lines.push(` import { ${importList.join(", ")} } from './sandbox-tools';`);
16835
+ if (pascalImports.length > 0) {
16836
+ for (const name of pascalImports) {
16837
+ lines.push(` import { ${name} } from "@granular/domain/${name}";`);
16838
+ }
16839
+ }
16840
+ if (effectNames.length > 0) {
16841
+ lines.push(
16842
+ ` import { ${effectNames.join(", ")} } from "@granular/actions/backend";`
16843
+ );
16844
+ }
16845
+ if (pascalImports.length === 0 && effectNames.length === 0) {
16846
+ lines.push(` // import generated classes from @granular/domain/<Class>`);
16731
16847
  } else {
16732
- lines.push(` // import generated classes/tools from './sandbox-tools'`);
16848
+ lines.push(` // import user-facing helpers from @granular/agent when needed`);
16733
16849
  }
16734
16850
  lines.push("");
16735
16851
  if (classes.length > 0) {
@@ -16817,7 +16933,10 @@ const job = await session.submitJob(\``);
16817
16933
  lines.push("");
16818
16934
  return lines.join("\n");
16819
16935
  }
16820
- var MANIFEST_GUIDE_RELATIVE = path3__namespace.join("docs", "granular-manifest.md");
16936
+ var MANIFEST_GUIDE_RELATIVE = path3__namespace.join(
16937
+ "docs",
16938
+ "granular-manifest.md"
16939
+ );
16821
16940
  var SANDBOX_DOC_FILENAME = "GRANULAR_SANDBOX.md";
16822
16941
  function getAgentsMdPath(projectRoot) {
16823
16942
  return path3__namespace.join(projectRoot, "AGENTS.md");
@@ -16839,7 +16958,9 @@ function writeManifestGuideFile(projectRoot, projectName) {
16839
16958
  }
16840
16959
  function writeAgentsMdFile(projectRoot) {
16841
16960
  const agentsPath = getAgentsMdPath(projectRoot);
16842
- const block = generateGranularAgentsBlock({ manifestGuidePath: MANIFEST_GUIDE_RELATIVE.replace(/\\/g, "/") });
16961
+ const block = generateGranularAgentsBlock({
16962
+ manifestGuidePath: MANIFEST_GUIDE_RELATIVE.replace(/\\/g, "/")
16963
+ });
16843
16964
  const previous = fs__namespace.existsSync(agentsPath) ? fs__namespace.readFileSync(agentsPath, "utf-8") : "";
16844
16965
  const merged = mergeGranularAgentsBlock(previous, block);
16845
16966
  fs__namespace.writeFileSync(agentsPath, merged, "utf-8");
@@ -18077,7 +18198,10 @@ async function initCommand(projectName, options) {
18077
18198
  }
18078
18199
  var DEFAULT_LOCAL_API_KEY2 = "gn_sk_tenant_default_principal_local_e2e_00000000";
18079
18200
  function promptSecret(question) {
18080
- const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
18201
+ const rl = readline__namespace.createInterface({
18202
+ input: process.stdin,
18203
+ output: process.stdout
18204
+ });
18081
18205
  return new Promise((resolve2) => {
18082
18206
  rl.question(` ${question}: `, (answer) => {
18083
18207
  rl.close();
@@ -18089,7 +18213,10 @@ function waitForEnter2(message) {
18089
18213
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
18090
18214
  return Promise.resolve();
18091
18215
  }
18092
- const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
18216
+ const rl = readline__namespace.createInterface({
18217
+ input: process.stdin,
18218
+ output: process.stdout
18219
+ });
18093
18220
  return new Promise((resolve2) => {
18094
18221
  rl.question(` ${message}`, () => {
18095
18222
  rl.close();
@@ -18119,14 +18246,20 @@ async function loginCommand(options = {}) {
18119
18246
  apiKey = existing?.trim() || process.env.GRANULAR_LOCAL_API_KEY?.trim() || DEFAULT_LOCAL_API_KEY2;
18120
18247
  info("Using local Granular API key for localhost development.");
18121
18248
  } else if (options.manual) {
18122
- dim("Get your API key at https://app.granular.software/w/default/api-keys");
18249
+ dim(
18250
+ "Get your API key at https://app.granular.software/w/default/api-keys"
18251
+ );
18123
18252
  console.log();
18124
18253
  apiKey = await promptSecret("Enter your API key");
18125
18254
  } else {
18126
18255
  const timeoutMs = options.timeout && options.timeout > 0 ? options.timeout * 1e3 : void 0;
18127
- await waitForEnter2("Press Enter to open the login page in your browser...");
18256
+ await waitForEnter2(
18257
+ "Press Enter to open the login page in your browser..."
18258
+ );
18128
18259
  console.log();
18129
- const waiting = spinner("Opening browser and waiting for authentication...");
18260
+ const waiting = spinner(
18261
+ "Opening browser and waiting for authentication..."
18262
+ );
18130
18263
  try {
18131
18264
  apiKey = await loginWithBrowser({
18132
18265
  authBaseUrl: loadAuthUrl(),
@@ -18144,7 +18277,9 @@ async function loginCommand(options = {}) {
18144
18277
  waiting.succeed(" Browser authentication completed.");
18145
18278
  } catch (err) {
18146
18279
  waiting.fail(` Browser authentication failed: ${err.message}`);
18147
- dim("Retry with `granular login --manual` to paste an API key directly.");
18280
+ dim(
18281
+ "Retry with `granular login --manual` to paste an API key directly."
18282
+ );
18148
18283
  process.exit(1);
18149
18284
  }
18150
18285
  }
@@ -18154,7 +18289,9 @@ async function loginCommand(options = {}) {
18154
18289
  process.exit(1);
18155
18290
  }
18156
18291
  if (!apiKey.startsWith("sk_") && !apiKey.startsWith("gn_sk_")) {
18157
- warn("API key does not look like a recognized Granular/WorkOS key. Continuing anyway...");
18292
+ warn(
18293
+ "API key does not look like a recognized Granular/WorkOS key. Continuing anyway..."
18294
+ );
18158
18295
  }
18159
18296
  const spinner2 = spinner("Validating...");
18160
18297
  const api = new ApiClient(apiKey, apiUrl);
@@ -18171,12 +18308,7 @@ async function loginCommand(options = {}) {
18171
18308
  console.log();
18172
18309
  }
18173
18310
  function cleanPermissionProfile(profile) {
18174
- const {
18175
- source,
18176
- sourcePath,
18177
- defaultsOwnerName,
18178
- ...rest
18179
- } = profile;
18311
+ const { source, sourcePath, defaultsOwnerName, ...rest } = profile;
18180
18312
  return rest;
18181
18313
  }
18182
18314
  function writePermissionProfileFiles(profiles) {
@@ -18221,14 +18353,16 @@ async function pullCommand(versionId) {
18221
18353
  writeManifestFile(project);
18222
18354
  spin.text = " Downloading permission profile files...";
18223
18355
  const profileVersions = await api.getVersionPermissionProfiles(versionId);
18224
- const pulledProfiles = (profileVersions.items || []).map((item) => item.profile).filter((profile) => Boolean(profile?.name));
18356
+ const pulledProfiles = (profileVersions.items || []).map((item) => item.profile).filter(
18357
+ (profile) => Boolean(profile?.name)
18358
+ );
18225
18359
  const writtenProfiles = writePermissionProfileFiles(pulledProfiles);
18226
18360
  spin.succeed(" Manifest pulled successfully.");
18227
18361
  console.log();
18228
18362
  keyValue({
18229
18363
  "Manifest ID": full.manifestId,
18230
18364
  "Source Version": versionId,
18231
- "Version": targetVersionLabel,
18365
+ Version: targetVersionLabel,
18232
18366
  "Written to": "granular.json",
18233
18367
  "Permission profiles": writtenProfiles.length > 0 ? writtenProfiles.join(", ") : "none"
18234
18368
  });
@@ -18263,32 +18397,51 @@ async function buildCommand() {
18263
18397
  }
18264
18398
  process.exit(1);
18265
18399
  }
18266
- const syncingProfiles = spinner(`Syncing ${permissionProfiles.length} permission profile file(s)...`);
18400
+ const syncingProfiles = spinner(
18401
+ `Syncing ${permissionProfiles.length} permission profile file(s)...`
18402
+ );
18267
18403
  try {
18268
- const result = await api.syncPermissionProfileSources(config.sandboxId, permissionProfiles);
18269
- syncingProfiles.succeed(` Permission profiles synced: ${brand.secondary(result.digest)}`);
18404
+ const result = await api.syncPermissionProfileSources(
18405
+ config.sandboxId,
18406
+ permissionProfiles
18407
+ );
18408
+ syncingProfiles.succeed(
18409
+ ` Permission profiles synced: ${brand.secondary(result.digest)}`
18410
+ );
18270
18411
  } catch (err) {
18271
- syncingProfiles.fail(` Failed to sync permission profiles: ${err.message}`);
18412
+ syncingProfiles.fail(
18413
+ ` Failed to sync permission profiles: ${err.message}`
18414
+ );
18272
18415
  process.exit(1);
18273
18416
  }
18274
18417
  const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
18275
18418
  let uploadedManifest;
18276
18419
  try {
18277
18420
  uploadedManifest = await api.uploadManifest(config.sandboxId, manifest);
18278
- uploading.succeed(` Manifest uploaded: ${brand.secondary(uploadedManifest.manifestId)}`);
18421
+ uploading.succeed(
18422
+ ` Manifest uploaded: ${brand.secondary(uploadedManifest.manifestId)}`
18423
+ );
18279
18424
  } catch (err) {
18280
18425
  uploading.fail(` Failed to upload manifest: ${err.message}`);
18281
18426
  process.exit(1);
18282
18427
  }
18283
- const building = spinner("Creating or reusing ontology version from manifest...");
18428
+ const building = spinner(
18429
+ "Creating or reusing ontology version from manifest..."
18430
+ );
18284
18431
  try {
18285
- const version2 = await api.createVersion(config.sandboxId, uploadedManifest.manifestId);
18432
+ const version2 = await api.createVersion(
18433
+ config.sandboxId,
18434
+ uploadedManifest.manifestId
18435
+ );
18286
18436
  building.text = ` Running build for version... ${brand.muted(version2.buildId)}`;
18287
18437
  const startTime = Date.now();
18288
- const completed = await api.waitForVersionBuild(version2.buildId, (status) => {
18289
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
18290
- building.text = ` Running build for version... ${brand.muted(`${status} (${elapsed}s)`)}`;
18291
- });
18438
+ const completed = await api.waitForVersionBuild(
18439
+ version2.buildId,
18440
+ (status) => {
18441
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
18442
+ building.text = ` Running build for version... ${brand.muted(`${status} (${elapsed}s)`)}`;
18443
+ }
18444
+ );
18292
18445
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
18293
18446
  building.succeed(` Version ready in ${totalTime}s`);
18294
18447
  const [tags, diff] = await Promise.all([
@@ -18308,11 +18461,11 @@ async function buildCommand() {
18308
18461
  "Version ID": completed.buildId,
18309
18462
  "Version Number": completed.versionNumber != null ? String(completed.versionNumber) : "N/A",
18310
18463
  "Build Run": completed.buildRunId || completed.latestBuildRunId || "N/A",
18311
- "Manifest": uploadedManifest.manifestId,
18312
- "Result": completed.createdNewVersion ? "new version created" : "existing version reused, new build run recorded",
18464
+ Manifest: uploadedManifest.manifestId,
18465
+ Result: completed.createdNewVersion ? "new version created" : "existing version reused, new build run recorded",
18313
18466
  "Dev Tag": devTag?.targetVersionId === completed.buildId || devTag?.targetBuildId === completed.buildId ? "updated to this version" : devTag?.targetVersionId || devTag?.targetBuildId || "not set",
18314
18467
  "Prod Tag": prodTag?.targetVersionId || prodTag?.targetBuildId || "not set",
18315
- "Duration": `${totalTime}s`,
18468
+ Duration: `${totalTime}s`,
18316
18469
  "Agent doc": "GRANULAR_SANDBOX.md"
18317
18470
  });
18318
18471
  if (diff?.diff?.summary) {
@@ -18325,7 +18478,9 @@ async function buildCommand() {
18325
18478
  });
18326
18479
  }
18327
18480
  console.log();
18328
- info("Common path: keep working in your dev environment while dev follows the newest successful version. Prod only changes when you promote it.");
18481
+ info(
18482
+ "Common path: keep working in your dev environment while dev follows the newest successful version. Prod only changes when you promote it."
18483
+ );
18329
18484
  console.log();
18330
18485
  } catch (err) {
18331
18486
  building.fail(` Build failed: ${err.message}`);
@@ -18342,7 +18497,10 @@ async function deployCommand(options = {}) {
18342
18497
  process.exit(1);
18343
18498
  }
18344
18499
  const api = new ApiClient(config.apiKey, config.apiUrl);
18345
- step(options.prod ? "Deploy to dev and prod" : "Deploy to dev", `Sandbox ${config.sandboxId}`);
18500
+ step(
18501
+ options.prod ? "Deploy to dev and prod" : "Deploy to dev",
18502
+ `Sandbox ${config.sandboxId}`
18503
+ );
18346
18504
  console.log();
18347
18505
  const manifest = config.project.manifest;
18348
18506
  const permissionProfiles = readPermissionProfileFiles();
@@ -18356,19 +18514,30 @@ async function deployCommand(options = {}) {
18356
18514
  for (const error2 of localPolicyValidation.errors) error(` ${error2}`);
18357
18515
  process.exit(1);
18358
18516
  }
18359
- const syncingProfiles = spinner(`Syncing ${permissionProfiles.length} permission profile file(s)...`);
18517
+ const syncingProfiles = spinner(
18518
+ `Syncing ${permissionProfiles.length} permission profile file(s)...`
18519
+ );
18360
18520
  try {
18361
- const result = await api.syncPermissionProfileSources(config.sandboxId, permissionProfiles);
18362
- syncingProfiles.succeed(` Permission profiles synced: ${brand.secondary(result.digest)}`);
18521
+ const result = await api.syncPermissionProfileSources(
18522
+ config.sandboxId,
18523
+ permissionProfiles
18524
+ );
18525
+ syncingProfiles.succeed(
18526
+ ` Permission profiles synced: ${brand.secondary(result.digest)}`
18527
+ );
18363
18528
  } catch (err) {
18364
- syncingProfiles.fail(` Failed to sync permission profiles: ${err.message}`);
18529
+ syncingProfiles.fail(
18530
+ ` Failed to sync permission profiles: ${err.message}`
18531
+ );
18365
18532
  process.exit(1);
18366
18533
  }
18367
18534
  const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
18368
18535
  let uploadedManifest;
18369
18536
  try {
18370
18537
  uploadedManifest = await api.uploadManifest(config.sandboxId, manifest);
18371
- uploading.succeed(` Manifest uploaded: ${brand.secondary(uploadedManifest.manifestId)}`);
18538
+ uploading.succeed(
18539
+ ` Manifest uploaded: ${brand.secondary(uploadedManifest.manifestId)}`
18540
+ );
18372
18541
  } catch (err) {
18373
18542
  uploading.fail(` Failed to upload manifest: ${err.message}`);
18374
18543
  process.exit(1);
@@ -18381,7 +18550,9 @@ async function deployCommand(options = {}) {
18381
18550
  ]);
18382
18551
  const devTag = tags.find((tag2) => tag2.name === "dev");
18383
18552
  const prodTag = tags.find((tag2) => tag2.name === "prod");
18384
- const existingVersion = versions.find((version2) => version2.manifestDigest === uploadedManifest.digest) || null;
18553
+ const existingVersion = versions.find(
18554
+ (version2) => version2.manifestDigest === uploadedManifest.digest
18555
+ ) || null;
18385
18556
  const existingVersionId = existingVersion?.buildId || null;
18386
18557
  const devVersionId = devTag?.targetVersionId || devTag?.targetBuildId || null;
18387
18558
  const reuseDevPromotion = Boolean(
@@ -18391,9 +18562,14 @@ async function deployCommand(options = {}) {
18391
18562
  let totalTime = 0;
18392
18563
  let deployedToDev = false;
18393
18564
  if (reuseDevPromotion) {
18394
- building.succeed(` Current manifest already matches ${brand.secondary(existingVersionId)} on dev.`);
18565
+ building.succeed(
18566
+ ` Current manifest already matches ${brand.secondary(existingVersionId)} on dev.`
18567
+ );
18395
18568
  } else {
18396
- const version2 = await api.createVersion(config.sandboxId, uploadedManifest.manifestId);
18569
+ const version2 = await api.createVersion(
18570
+ config.sandboxId,
18571
+ uploadedManifest.manifestId
18572
+ );
18397
18573
  const startTime = Date.now();
18398
18574
  building.text = ` Running build for version... ${brand.muted(version2.buildId)}`;
18399
18575
  completed = await api.waitForVersionBuild(version2.buildId, (status) => {
@@ -18402,7 +18578,9 @@ async function deployCommand(options = {}) {
18402
18578
  });
18403
18579
  totalTime = Math.round((Date.now() - startTime) / 1e3);
18404
18580
  deployedToDev = true;
18405
- building.succeed(` Dev now points to ${brand.secondary(completed.buildId)} (${totalTime}s)`);
18581
+ building.succeed(
18582
+ ` Dev now points to ${brand.secondary(completed.buildId)} (${totalTime}s)`
18583
+ );
18406
18584
  }
18407
18585
  if (!completed) {
18408
18586
  throw new Error("Could not resolve the current ontology version.");
@@ -18410,7 +18588,9 @@ async function deployCommand(options = {}) {
18410
18588
  if (options.prod) {
18411
18589
  const promoting = spinner("Promoting prod...");
18412
18590
  await api.promoteVersionToProd(completed.buildId);
18413
- promoting.succeed(` Prod now points to ${brand.secondary(completed.buildId)}`);
18591
+ promoting.succeed(
18592
+ ` Prod now points to ${brand.secondary(completed.buildId)}`
18593
+ );
18414
18594
  }
18415
18595
  writeSandboxAgentDocFile(getProjectRoot(), manifest, {
18416
18596
  sandboxId: config.sandboxId,
@@ -18422,18 +18602,20 @@ async function deployCommand(options = {}) {
18422
18602
  success(`Deployed ${brand.bold(manifest.name)} successfully!`);
18423
18603
  console.log();
18424
18604
  keyValue({
18425
- "Version": completed.buildId,
18605
+ Version: completed.buildId,
18426
18606
  "Version Number": completed.versionNumber != null ? String(completed.versionNumber) : "N/A",
18427
18607
  "Build Run": completed.buildRunId || completed.latestBuildRunId || "N/A",
18428
- "Manifest": uploadedManifest.manifestId,
18608
+ Manifest: uploadedManifest.manifestId,
18429
18609
  "Dev Tag": completed.buildId,
18430
18610
  "Prod Tag": options.prod ? completed.buildId : prodTag?.targetVersionId || prodTag?.targetBuildId || "unchanged",
18431
- "Release": options.prod ? deployedToDev ? "dev and prod now point to this version" : "prod promoted to the version already on dev" : "dev updated to this version",
18611
+ Release: options.prod ? deployedToDev ? "dev and prod now point to this version" : "prod promoted to the version already on dev" : "dev updated to this version",
18432
18612
  "Agent doc": "GRANULAR_SANDBOX.md"
18433
18613
  });
18434
18614
  console.log();
18435
18615
  if (options.prod) {
18436
- info("Existing prod environments keep their current version until they are transitioned.");
18616
+ info(
18617
+ "Existing prod environments keep their current version until they are transitioned."
18618
+ );
18437
18619
  console.log();
18438
18620
  }
18439
18621
  } catch (err) {
@@ -18445,7 +18627,9 @@ async function deployCommand(options = {}) {
18445
18627
  // src/cli/commands/status.ts
18446
18628
  function summarizeLocalManifest(project) {
18447
18629
  const manifest = project.manifest;
18448
- const classCount = manifest.volumes.flatMap((volume) => volume.operations).filter((operation) => operation.create && !operation.defineRelationship).length;
18630
+ const classCount = manifest.volumes.flatMap((volume) => volume.operations).filter(
18631
+ (operation) => operation.create && !operation.defineRelationship
18632
+ ).length;
18449
18633
  const relationshipCount = manifest.volumes.flatMap((volume) => volume.operations).filter((operation) => operation.defineRelationship).length;
18450
18634
  return {
18451
18635
  name: manifest.name,
@@ -18515,8 +18699,8 @@ async function statusCommand(options = {}) {
18515
18699
  console.log();
18516
18700
  keyValue({
18517
18701
  "Sandbox ID": sandbox.sandboxId,
18518
- "Name": sandbox.name,
18519
- "Created": sandbox.createdAt
18702
+ Name: sandbox.name,
18703
+ Created: sandbox.createdAt
18520
18704
  });
18521
18705
  console.log();
18522
18706
  keyValue({
@@ -18557,9 +18741,9 @@ async function statusCommand(options = {}) {
18557
18741
  step("Local Manifest", `granular.json`);
18558
18742
  console.log();
18559
18743
  keyValue({
18560
- "Name": payload.localManifest.name,
18561
- "Classes": payload.localManifest.classCount,
18562
- "Relationships": payload.localManifest.relationshipCount
18744
+ Name: payload.localManifest.name,
18745
+ Classes: payload.localManifest.classCount,
18746
+ Relationships: payload.localManifest.relationshipCount
18563
18747
  });
18564
18748
  }
18565
18749
  if (options.verbose) {
@@ -18580,7 +18764,10 @@ async function statusCommand(options = {}) {
18580
18764
  }
18581
18765
  }
18582
18766
  function prompt2(question, defaultValue) {
18583
- const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
18767
+ const rl = readline__namespace.createInterface({
18768
+ input: process.stdin,
18769
+ output: process.stdout
18770
+ });
18584
18771
  const suffix = defaultValue ? ` ${brand.muted(`(${defaultValue})`)}` : "";
18585
18772
  return new Promise((resolve2) => {
18586
18773
  rl.question(` ${question}${suffix}: `, (answer) => {
@@ -18616,7 +18803,10 @@ async function addClassCommand(name) {
18616
18803
  const fieldName = await prompt2("Field name (leave empty to finish)");
18617
18804
  if (!fieldName) break;
18618
18805
  const fieldType = await prompt2("Type", "string");
18619
- const description = await prompt2("Description", `${fieldName} of the ${name}`);
18806
+ const description = await prompt2(
18807
+ "Description",
18808
+ `${fieldName} of the ${name}`
18809
+ );
18620
18810
  fields[fieldName] = {
18621
18811
  type: fieldType,
18622
18812
  description
@@ -18635,9 +18825,7 @@ async function addClassCommand(name) {
18635
18825
  manifest.volumes.push({
18636
18826
  name: "schema",
18637
18827
  scope: "sandbox",
18638
- imports: [
18639
- { alias: "@std", name: "standard_modules", label: "prod" }
18640
- ],
18828
+ imports: [{ alias: "@std", name: "standard_modules", label: "prod" }],
18641
18829
  operations: []
18642
18830
  });
18643
18831
  }
@@ -18647,7 +18835,10 @@ async function addClassCommand(name) {
18647
18835
  success(`Class ${brand.bold(name)} added to granular.json`);
18648
18836
  dim(`${Object.keys(fields).length} field(s) defined`);
18649
18837
  nextSteps([
18650
- { command: `granular add field ${name} <field>`, description: "Add more fields" },
18838
+ {
18839
+ command: `granular add field ${name} <field>`,
18840
+ description: "Add more fields"
18841
+ },
18651
18842
  { command: "granular add relation", description: "Define a relationship" },
18652
18843
  { command: "granular build", description: "Build your changes" }
18653
18844
  ]);
@@ -18672,14 +18863,19 @@ async function addFieldCommand(className, fieldName) {
18672
18863
  process.exit(1);
18673
18864
  }
18674
18865
  const fieldType = await prompt2("Type", "string");
18675
- const description = await prompt2("Description", `${fieldName} of the ${className}`);
18866
+ const description = await prompt2(
18867
+ "Description",
18868
+ `${fieldName} of the ${className}`
18869
+ );
18676
18870
  if (!classOp.has) classOp.has = {};
18677
18871
  classOp.has[fieldName] = {
18678
18872
  type: fieldType,
18679
18873
  description
18680
18874
  };
18681
18875
  writeManifestFile(project);
18682
- success(`Field ${brand.bold(fieldName)} (${fieldType}) added to ${brand.bold(className)}`);
18876
+ success(
18877
+ `Field ${brand.bold(fieldName)} (${fieldType}) added to ${brand.bold(className)}`
18878
+ );
18683
18879
  nextSteps([
18684
18880
  { command: "granular build", description: "Build your changes" }
18685
18881
  ]);
@@ -18708,10 +18904,19 @@ async function addRelationCommand() {
18708
18904
  error(`Class "${right}" not found.`);
18709
18905
  process.exit(1);
18710
18906
  }
18711
- const leftSubmodel = await prompt2(`${left}'s reference to ${right} (plural)`, `${right}s`);
18907
+ const leftSubmodel = await prompt2(
18908
+ `${left}'s reference to ${right} (plural)`,
18909
+ `${right}s`
18910
+ );
18712
18911
  const rightSubmodel = await prompt2(`${right}'s reference to ${left}`, left);
18713
- const leftIsMany = await confirm2(`Can a ${left} have multiple ${leftSubmodel}?`, true);
18714
- const rightIsMany = await confirm2(`Can a ${right} have multiple ${rightSubmodel}?`, false);
18912
+ const leftIsMany = await confirm2(
18913
+ `Can a ${left} have multiple ${leftSubmodel}?`,
18914
+ true
18915
+ );
18916
+ const rightIsMany = await confirm2(
18917
+ `Can a ${right} have multiple ${rightSubmodel}?`,
18918
+ false
18919
+ );
18715
18920
  const operation = {
18716
18921
  defineRelationship: {
18717
18922
  left,
@@ -18725,8 +18930,12 @@ async function addRelationCommand() {
18725
18930
  manifest.volumes[0].operations.push(operation);
18726
18931
  writeManifestFile(project);
18727
18932
  console.log();
18728
- success(`Relationship defined: ${brand.bold(left)} \u2194 ${brand.bold(right)}`);
18729
- dim(`${left}.${leftSubmodel} (${leftIsMany ? "many" : "one"}) \u2194 ${right}.${rightSubmodel} (${rightIsMany ? "many" : "one"})`);
18933
+ success(
18934
+ `Relationship defined: ${brand.bold(left)} \u2194 ${brand.bold(right)}`
18935
+ );
18936
+ dim(
18937
+ `${left}.${leftSubmodel} (${leftIsMany ? "many" : "one"}) \u2194 ${right}.${rightSubmodel} (${rightIsMany ? "many" : "one"})`
18938
+ );
18730
18939
  nextSteps([
18731
18940
  { command: "granular build", description: "Build your changes" }
18732
18941
  ]);
@@ -18760,7 +18969,7 @@ async function whoamiCommand() {
18760
18969
  try {
18761
18970
  const sandbox = await api.getSandbox(rc.sandboxId);
18762
18971
  keyValue({
18763
- "Sandbox": sandbox.name || sandbox.sandboxId,
18972
+ Sandbox: sandbox.name || sandbox.sandboxId,
18764
18973
  "Sandbox ID": sandbox.sandboxId,
18765
18974
  "API URL": config.apiUrl
18766
18975
  });
@@ -18802,16 +19011,24 @@ async function devCommand() {
18802
19011
  return;
18803
19012
  }
18804
19013
  const spin = spinner("Uploading manifest...");
18805
- const uploaded = await api.uploadManifest(config.sandboxId, project.manifest);
19014
+ const uploaded = await api.uploadManifest(
19015
+ config.sandboxId,
19016
+ project.manifest
19017
+ );
18806
19018
  spin.text = " Building...";
18807
- const build = await api.triggerBuild(config.sandboxId, uploaded.manifestId);
19019
+ const build = await api.triggerBuild(
19020
+ config.sandboxId,
19021
+ uploaded.manifestId
19022
+ );
18808
19023
  const startTime = Date.now();
18809
19024
  const completed = await api.waitForBuild(build.buildId, (status) => {
18810
19025
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
18811
19026
  spin.text = ` Building... ${brand.muted(`${status} (${elapsed}s)`)}`;
18812
19027
  });
18813
19028
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
18814
- spin.succeed(` Build completed in ${totalTime}s \u2014 ${brand.secondary(completed.buildId)}`);
19029
+ spin.succeed(
19030
+ ` Build completed in ${totalTime}s \u2014 ${brand.secondary(completed.buildId)}`
19031
+ );
18815
19032
  writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
18816
19033
  sandboxId: config.sandboxId,
18817
19034
  apiUrl: config.apiUrl,
@@ -18860,7 +19077,9 @@ async function documentCommand() {
18860
19077
  buildPending: true
18861
19078
  });
18862
19079
  spinner2.succeed(`Generated ${brand.bold("GRANULAR_SANDBOX.md")}`);
18863
- info("This file lists classes, relationships, and effects from granular.json. After a successful build, run again or use `granular build` to fill in manifest and build ids.");
19080
+ info(
19081
+ "This file lists classes, relationships, and effects from granular.json. After a successful build, run again or use `granular build` to fill in manifest and build ids."
19082
+ );
18864
19083
  }
18865
19084
  var SIMULATOR_BASE = "https://app.granular.software/simulator";
18866
19085
  function openUrl(url) {
@@ -18871,7 +19090,9 @@ function openUrl(url) {
18871
19090
  async function simulateCommand(sandboxIdArg, options) {
18872
19091
  const sandboxId = sandboxIdArg ?? resolveConfig().sandboxId;
18873
19092
  if (!sandboxId) {
18874
- error("No ontology ID. Run from a project with `granular init` or pass one explicitly: granular simulate <ontology-id>");
19093
+ error(
19094
+ "No ontology ID. Run from a project with `granular init` or pass one explicitly: granular simulate <ontology-id>"
19095
+ );
18875
19096
  process.exit(1);
18876
19097
  }
18877
19098
  const params = new URLSearchParams({
@@ -18909,6 +19130,7 @@ var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
18909
19130
  var DEFAULT_RPC_TIMEOUT_MS = 3e4;
18910
19131
  var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
18911
19132
  var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
19133
+ var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
18912
19134
  var DEFAULT_RECONNECT_DELAY_MS = 3e3;
18913
19135
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
18914
19136
  function debugWs(...args) {
@@ -18925,6 +19147,8 @@ function rpcTimeoutMsForMethod(method) {
18925
19147
  case "effects.publishCatalog":
18926
19148
  case "effects.refresh":
18927
19149
  return EFFECT_CONTROL_RPC_TIMEOUT_MS;
19150
+ case "harness.run":
19151
+ return HARNESS_RUN_RPC_TIMEOUT_MS;
18928
19152
  default:
18929
19153
  return DEFAULT_RPC_TIMEOUT_MS;
18930
19154
  }
@@ -19590,7 +19814,9 @@ function scorePromptChoiceMatch(answer, answerTokens, option) {
19590
19814
  const choice = normalizePromptChoiceOption(option);
19591
19815
  const { value, label } = choice;
19592
19816
  const description = choice.description || "";
19593
- const haystack = normalizePromptText([value, label, description].filter(Boolean).join(" "));
19817
+ const haystack = normalizePromptText(
19818
+ [value, label, description].filter(Boolean).join(" ")
19819
+ );
19594
19820
  if (!haystack) return { score: 0, resolvedValue: value || label || null };
19595
19821
  let score = 0;
19596
19822
  if (value && normalizePromptText(value) === answer) score += 12;
@@ -19600,7 +19826,8 @@ function scorePromptChoiceMatch(answer, answerTokens, option) {
19600
19826
  for (const token of answerTokens) {
19601
19827
  if (value && normalizePromptText(value).includes(token)) score += 10;
19602
19828
  if (label && normalizePromptText(label).includes(token)) score += 8;
19603
- if (description && normalizePromptText(description).includes(token)) score += 5;
19829
+ if (description && normalizePromptText(description).includes(token))
19830
+ score += 5;
19604
19831
  }
19605
19832
  return { score, resolvedValue: value || label || null };
19606
19833
  }
@@ -19610,7 +19837,8 @@ function normalizePromptType(raw) {
19610
19837
  const promptType = typeof raw?.promptType === "string" ? raw.promptType : null;
19611
19838
  if (type === "confirm" || type === "choice" || type === "input") return type;
19612
19839
  if (kind === "confirm" || kind === "choice" || kind === "input") return kind;
19613
- if (promptType === "confirm" || promptType === "choice" || promptType === "input") return promptType;
19840
+ if (promptType === "confirm" || promptType === "choice" || promptType === "input")
19841
+ return promptType;
19614
19842
  return "input";
19615
19843
  }
19616
19844
  function normalizePrompt(rawValue) {
@@ -19626,7 +19854,9 @@ function normalizePrompt(rawValue) {
19626
19854
  title: typeof source.title === "string" ? source.title : "Input required",
19627
19855
  message: typeof source.message === "string" ? source.message : "",
19628
19856
  options: Array.isArray(source.options) ? source.options.map(
19629
- (option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(option) : option
19857
+ (option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(
19858
+ option
19859
+ ) : option
19630
19860
  ) : void 0,
19631
19861
  defaultValue: source.defaultValue,
19632
19862
  placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
@@ -19638,13 +19868,17 @@ function resolvePromptAnswer(prompt3, answer) {
19638
19868
  if (!prompt3) return answer;
19639
19869
  if (prompt3.type === "confirm") {
19640
19870
  if (typeof answer === "boolean") return answer;
19641
- if (typeof answer === "string") return /^(yes|y|true|confirm|ok)/i.test(answer.trim());
19871
+ if (typeof answer === "string")
19872
+ return /^(yes|y|true|confirm|ok)/i.test(answer.trim());
19642
19873
  return Boolean(answer);
19643
19874
  }
19644
19875
  if (prompt3.type === "choice" && Array.isArray(prompt3.options) && typeof answer === "string") {
19645
19876
  const normalized = normalizePromptText(answer);
19646
19877
  const tokens = extractPromptTokens(answer);
19647
- let best = { score: -1, resolvedValue: null };
19878
+ let best = {
19879
+ score: -1,
19880
+ resolvedValue: null
19881
+ };
19648
19882
  for (const option of prompt3.options) {
19649
19883
  const scored = scorePromptChoiceMatch(normalized, tokens, option);
19650
19884
  if (scored.score > best.score) best = scored;
@@ -19820,9 +20054,11 @@ var Session = class {
19820
20054
  /**
19821
20055
  * Submit a job to execute code in the sandbox.
19822
20056
  *
19823
- * The code can import typed classes from `./sandbox-tools`:
20057
+ * The code can import typed classes from Harness v3 runtime modules:
19824
20058
  * ```typescript
19825
- * import { Author, Book, global_search } from './sandbox-tools';
20059
+ * import { Author } from "@granular/domain/Author";
20060
+ * import { Book } from "@granular/domain/Book";
20061
+ * import { global_search } from "@granular/actions/backend";
19826
20062
  *
19827
20063
  * const totalAuthors = await Author.count();
19828
20064
  * const firstAuthorsPage = await Author.page({ page: 1, perPage: 10, saveAs: 'recent_authors' });
@@ -19900,7 +20136,11 @@ var Session = class {
19900
20136
  const resolvedAnswer = resolvePromptAnswer(prompt3, answer);
19901
20137
  this.promptCache.delete(promptId);
19902
20138
  this.hiddenPromptIds.add(promptId);
19903
- this.emit("prompt", { id: promptId, status: "answered" });
20139
+ this.emit("prompt:answered", {
20140
+ ...prompt3 || { id: promptId },
20141
+ id: promptId,
20142
+ status: "answered"
20143
+ });
19904
20144
  try {
19905
20145
  const response = await this.client.call("prompt.answer", {
19906
20146
  promptId,
@@ -20211,14 +20451,19 @@ var Session = class {
20211
20451
  const tools = summary.tools || [];
20212
20452
  if (classes && Object.keys(classes).length > 0) {
20213
20453
  let docs2 = "# Domain Documentation\n\n";
20214
- docs2 += "Import classes and tools from `./sandbox-tools`:\n\n";
20454
+ docs2 += "Import concrete classes from `@granular/domain/<Class>` and global backend actions from `@granular/actions/backend`:\n\n";
20215
20455
  const classNames = Object.keys(classes).map(
20216
20456
  (c) => c.charAt(0).toUpperCase() + c.slice(1)
20217
20457
  );
20218
20458
  const globalNames = (globalTools || []).map((t) => t.name);
20219
- const allImports = [...classNames, ...globalNames].join(", ");
20459
+ const importLines = [
20460
+ ...classNames.map(
20461
+ (name) => `import { ${name} } from "@granular/domain/${name}";`
20462
+ ),
20463
+ globalNames.length > 0 ? `import { ${globalNames.join(", ")} } from "@granular/actions/backend";` : null
20464
+ ].filter(Boolean);
20220
20465
  docs2 += `\`\`\`typescript
20221
- import { ${allImports} } from "./sandbox-tools";
20466
+ ${importLines.join("\n") || "// No generated domain imports available."}
20222
20467
  \`\`\`
20223
20468
 
20224
20469
  `;
@@ -20282,10 +20527,13 @@ import { ${allImports} } from "./sandbox-tools";
20282
20527
  return "No effects available in this domain.";
20283
20528
  }
20284
20529
  let docs = "# Available Effects\n\n";
20285
- docs += "Import effects from `./sandbox-tools` and call them with await:\n\n";
20286
- docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
20530
+ docs += "Import global backend actions from `@granular/actions/backend` and call them with await:\n\n";
20531
+ docs += `\`\`\`typescript
20532
+ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
20533
+
20534
+ `;
20287
20535
  docs += "// Example:\n";
20288
- docs += `const result = await tools.${tools[0]?.name || "example"}(input);
20536
+ docs += `const result = await ${tools[0]?.name || "example"}(input);
20289
20537
  `;
20290
20538
  docs += "```\n\n";
20291
20539
  for (const tool of tools) {
@@ -20416,7 +20664,7 @@ import { ${allImports} } from "./sandbox-tools";
20416
20664
  const prompt3 = normalizePrompt(payload);
20417
20665
  if (!prompt3) return;
20418
20666
  if (this.hiddenPromptIds.has(prompt3.id)) {
20419
- this.emit("prompt", { ...prompt3, status: "answered" });
20667
+ this.emit("prompt:answered", { ...prompt3, status: "answered" });
20420
20668
  return;
20421
20669
  }
20422
20670
  this.promptCache.set(prompt3.id, prompt3);
@@ -20435,9 +20683,16 @@ import { ${allImports} } from "./sandbox-tools";
20435
20683
  this.client.on("job.status", (data) => {
20436
20684
  this.emit("job:status", data);
20437
20685
  });
20686
+ this.client.on("harness.ui_status", (data) => {
20687
+ this.emit("harness:ui_status", data);
20688
+ });
20689
+ this.client.on("harness.model_stream", (data) => {
20690
+ this.emit("harness:model_stream", data);
20691
+ });
20438
20692
  this.client.on("job.agent_message", (data) => {
20439
20693
  const normalized = normalizeJobAgentMessageEnvelope(data);
20440
20694
  if (!normalized) return;
20695
+ this.emit("job:agent_message", normalized);
20441
20696
  if (this.jobsMap.has(normalized.jobId)) return;
20442
20697
  const pending = this.pendingAgentMessagesByJobId.get(normalized.jobId) || [];
20443
20698
  if (normalized.message.messageId && pending.some(
@@ -20587,6 +20842,7 @@ function normalizeJobAgentMessageEnvelope(data) {
20587
20842
  kind: d.kind === "artifacts" ? "artifacts" : "text",
20588
20843
  reply: typeof d.reply === "string" ? d.reply : "",
20589
20844
  show: d.show,
20845
+ actions: Array.isArray(d.actions) ? d.actions : void 0,
20590
20846
  timestamp: d.timestamp || Date.now()
20591
20847
  }
20592
20848
  };
@@ -21598,7 +21854,9 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
21598
21854
  return void 0;
21599
21855
  }
21600
21856
  function resolveHandlerForMode(effectMap, effect, request) {
21601
- const behaviors = normalizeEffectBehaviors(request.context?.behaviors || effect.metamodels || void 0);
21857
+ const behaviors = normalizeEffectBehaviors(
21858
+ request.context?.behaviors || effect.metamodels || void 0
21859
+ );
21602
21860
  const mode = resolveInvocationMode(request.context);
21603
21861
  if (mode === "dryRun") {
21604
21862
  if (effect.dryRunHandler) {
@@ -21613,7 +21871,12 @@ function resolveHandlerForMode(effectMap, effect, request) {
21613
21871
  if (effect.reverseHandler) {
21614
21872
  return { effect, mode, handler: effect.reverseHandler };
21615
21873
  }
21616
- const reverseEffect = resolveReverseEffect(effectMap, effect, request, behaviors);
21874
+ const reverseEffect = resolveReverseEffect(
21875
+ effectMap,
21876
+ effect,
21877
+ request,
21878
+ behaviors
21879
+ );
21617
21880
  if (reverseEffect) {
21618
21881
  return {
21619
21882
  effect: reverseEffect,
@@ -21621,7 +21884,9 @@ function resolveHandlerForMode(effectMap, effect, request) {
21621
21884
  handler: reverseEffect.reverseHandler || reverseEffect.handler
21622
21885
  };
21623
21886
  }
21624
- throw new Error(`Reverse execution is not supported for ${request.effectKey}`);
21887
+ throw new Error(
21888
+ `Reverse execution is not supported for ${request.effectKey}`
21889
+ );
21625
21890
  }
21626
21891
  return { effect, mode, handler: effect.handler };
21627
21892
  }
@@ -21637,7 +21902,9 @@ async function invokeRegisteredEffect(effectMap, request) {
21637
21902
  const resolved = resolveHandlerForMode(effectMap, effect, request);
21638
21903
  const context = {
21639
21904
  ...request.context || {},
21640
- behaviors: normalizeEffectBehaviors(request.context?.behaviors || effect.metamodels || void 0),
21905
+ behaviors: normalizeEffectBehaviors(
21906
+ request.context?.behaviors || effect.metamodels || void 0
21907
+ ),
21641
21908
  invocation: {
21642
21909
  mode: resolved.mode,
21643
21910
  sourceEffectKey: request.effectKey,
@@ -21799,7 +22066,7 @@ function isRetryableRecordObjectsError(error2) {
21799
22066
  }
21800
22067
  function isRetryableEffectRegistrationError(error2) {
21801
22068
  const message = error2 instanceof Error ? error2.message : String(error2);
21802
- return /timed out|websocket disconnected|websocket not connected|rpc timeout|worker restarted mid-request|network connection lost|bad gateway|gateway timeout|too many requests|(?:control plane|granular|graphql) api error \((?:429|500|502|503|504)\)/i.test(
22069
+ return /timed out|websocket disconnected|websocket not connected|rpc timeout|rpc error: internal error; reference|worker restarted mid-request|network connection lost|bad gateway|gateway timeout|too many requests|(?:control plane|granular|graphql) api error \((?:429|500|502|503|504)\)/i.test(
21803
22070
  message
21804
22071
  );
21805
22072
  }
@@ -22033,19 +22300,19 @@ function computeEffectRegistrationKey(effect) {
22033
22300
  function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId, effectHostUrl) {
22034
22301
  const overrideUrl = effectHostUrl || process.env.GRANULAR_EFFECT_HOST_URL || process.env.EFFECT_HOST_URL;
22035
22302
  const api = new URL(apiUrl);
22036
- const localRuntimeBase = process.env.RUNTIME_ORCHESTRATOR_URL || (isLocalControlUrl(apiUrl) ? `${api.protocol}//${api.hostname}:8791` : "");
22303
+ const localRuntimeBase = process.env.RUNTIME_ORCHESTRATOR_URL || "";
22037
22304
  const url = new URL(overrideUrl || localRuntimeBase || apiUrl);
22038
22305
  if (url.protocol === "https:") {
22039
22306
  url.protocol = "wss:";
22040
22307
  } else if (url.protocol === "http:") {
22041
22308
  url.protocol = "ws:";
22042
22309
  }
22043
- if (!overrideUrl && isLocalControlUrl(apiUrl) && api.pathname.endsWith("/granular")) {
22044
- url.pathname = "/granular/orchestrator/effects/connect";
22310
+ if (!overrideUrl && isLocalControlUrl(apiUrl) && !localRuntimeBase && api.pathname.endsWith("/granular")) {
22311
+ url.pathname = "/granular/effects/connect";
22045
22312
  } else if (url.pathname.endsWith("/granular/ws/connect")) {
22046
22313
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
22047
22314
  } else if (url.pathname.endsWith("/granular")) {
22048
- url.pathname = isLocalControlUrl(url.toString()) ? "/granular/orchestrator/effects/connect" : `${url.pathname.replace(/\/$/, "")}/effects/connect`;
22315
+ url.pathname = localRuntimeBase && isLocalControlUrl(url.toString()) ? "/granular/orchestrator/effects/connect" : `${url.pathname.replace(/\/$/, "")}/effects/connect`;
22049
22316
  } else if (url.pathname.endsWith("/v2/ws/connect")) {
22050
22317
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
22051
22318
  } else if (url.pathname.endsWith("/v2/ws")) {
@@ -22226,7 +22493,15 @@ var Environment = class _Environment {
22226
22493
  create: async (options) => this.createSession(options),
22227
22494
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
22228
22495
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
22229
- close: async (sessionId, session2) => this.closeSession(sessionId, session2)
22496
+ close: async (sessionId, session2) => this.closeSession(sessionId, session2),
22497
+ state: async (options) => this.getUserEnvironmentState(options),
22498
+ markRead: async (options) => this.markUserEnvironmentSessionsRead(options)
22499
+ };
22500
+ }
22501
+ get userEnvironmentState() {
22502
+ return {
22503
+ get: async (options) => this.getUserEnvironmentState(options),
22504
+ markRead: async (options) => this.markUserEnvironmentSessionsRead(options)
22230
22505
  };
22231
22506
  }
22232
22507
  get data() {
@@ -22267,6 +22542,18 @@ var Environment = class _Environment {
22267
22542
  }
22268
22543
  return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
22269
22544
  }
22545
+ async getUserEnvironmentState(options = {}) {
22546
+ return this.granular.getUserEnvironmentState({
22547
+ ...options,
22548
+ environmentId: this.environmentId
22549
+ });
22550
+ }
22551
+ async markUserEnvironmentSessionsRead(options) {
22552
+ return this.granular.markUserEnvironmentSessionsRead({
22553
+ ...options,
22554
+ environmentId: this.environmentId
22555
+ });
22556
+ }
22270
22557
  async createSession(options) {
22271
22558
  return this.granular.createSession({
22272
22559
  environmentId: this.environmentId,
@@ -22277,7 +22564,9 @@ var Environment = class _Environment {
22277
22564
  async connectSession(sessionId, options) {
22278
22565
  const session2 = await this.granular["connectSession"]({
22279
22566
  sessionId,
22280
- clientId: options?.clientId
22567
+ clientId: options?.clientId,
22568
+ maxReconnectAttempts: options?.maxReconnectAttempts,
22569
+ reconnectDelayMs: options?.reconnectDelayMs
22281
22570
  });
22282
22571
  if (session2.environmentId !== this.environmentId) {
22283
22572
  await session2.disconnect().catch(() => {
@@ -24072,6 +24361,39 @@ var Granular = class _Granular {
24072
24361
  async listClosedSessions(filters) {
24073
24362
  return this.listSessionsForEnvironment(filters.environmentId, "closed");
24074
24363
  }
24364
+ async getUserEnvironmentState(options) {
24365
+ const query = new URLSearchParams({
24366
+ environmentId: options.environmentId
24367
+ });
24368
+ if (options.sessionScope) {
24369
+ query.set("sessionScope", options.sessionScope);
24370
+ }
24371
+ if (options.status) {
24372
+ query.set("status", options.status);
24373
+ }
24374
+ if (typeof options.limit === "number") {
24375
+ query.set("limit", String(options.limit));
24376
+ }
24377
+ if (typeof options.offset === "number") {
24378
+ query.set("offset", String(options.offset));
24379
+ }
24380
+ const state = await this.request(
24381
+ `/sdk/user-environment-state?${query.toString()}`
24382
+ );
24383
+ return this.normalizeUserEnvironmentState(state);
24384
+ }
24385
+ async markUserEnvironmentSessionsRead(options) {
24386
+ const result = await this.request("/sdk/user-environment-state/read", {
24387
+ method: "POST",
24388
+ body: JSON.stringify({
24389
+ environmentId: options.environmentId,
24390
+ sessionId: options.sessionId,
24391
+ sessionIds: options.sessionIds,
24392
+ readAt: options.readAt
24393
+ })
24394
+ });
24395
+ return result.readAtBySessionId || {};
24396
+ }
24075
24397
  async listSessionsForEnvironment(environmentId, status) {
24076
24398
  const query = new URLSearchParams({ environmentId, status });
24077
24399
  const res = await this.request(
@@ -24102,6 +24424,24 @@ var Granular = class _Granular {
24102
24424
  toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
24103
24425
  };
24104
24426
  }
24427
+ normalizeUserEnvironmentState(state) {
24428
+ return {
24429
+ ...state,
24430
+ sessions: Array.isArray(state.sessions) ? state.sessions.map((item) => ({
24431
+ ...item,
24432
+ session: this.normalizeConversationSession(
24433
+ item.session
24434
+ )
24435
+ })) : [],
24436
+ attention: {
24437
+ prompts: Array.isArray(state.attention?.prompts) ? state.attention.prompts : [],
24438
+ count: typeof state.attention?.count === "number" ? state.attention.count : 0,
24439
+ activePrompt: state.attention?.activePrompt || null
24440
+ },
24441
+ unreadCount: typeof state.unreadCount === "number" ? state.unreadCount : 0,
24442
+ readAtBySessionId: state.readAtBySessionId || {}
24443
+ };
24444
+ }
24105
24445
  static coerceIsoDate(value) {
24106
24446
  if (value instanceof Date) {
24107
24447
  return value.toISOString();
@@ -24144,7 +24484,10 @@ var Granular = class _Granular {
24144
24484
  });
24145
24485
  const envData = await this.environments.get(minted.environmentId);
24146
24486
  const environment = this.bindEnvironmentHandle(envData);
24147
- return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
24487
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted, {
24488
+ maxReconnectAttempts: options.maxReconnectAttempts,
24489
+ reconnectDelayMs: options.reconnectDelayMs
24490
+ });
24148
24491
  }
24149
24492
  async recordOpenAIUsageSpend(usage, context, options) {
24150
24493
  return recordOpenAIUsageSpend({
@@ -24295,13 +24638,15 @@ var Granular = class _Granular {
24295
24638
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
24296
24639
  return new Environment(this, envData, this.apiKey, graphqlEndpoint);
24297
24640
  }
24298
- async bindWebSocketEnvironmentSession(environment, clientId, session2) {
24641
+ async bindWebSocketEnvironmentSession(environment, clientId, session2, transportOptions = {}) {
24299
24642
  const client = new WSClient({
24300
24643
  url: session2.wsUrl,
24301
24644
  sessionId: session2.sessionId,
24302
24645
  token: session2.token,
24303
24646
  tokenProvider: this.tokenProvider,
24304
24647
  WebSocketCtor: this.WebSocketCtor,
24648
+ maxReconnectAttempts: transportOptions.maxReconnectAttempts,
24649
+ reconnectDelayMs: transportOptions.reconnectDelayMs,
24305
24650
  onUnexpectedClose: this.onUnexpectedClose,
24306
24651
  onReconnectError: this.onReconnectError
24307
24652
  });
@@ -24668,7 +25013,10 @@ var Granular = class _Granular {
24668
25013
  try {
24669
25014
  const sandbox = await this.sandboxes.get(nameOrId);
24670
25015
  return sandbox;
24671
- } catch {
25016
+ } catch (error2) {
25017
+ if (nameOrId.startsWith("sbx_")) {
25018
+ throw error2;
25019
+ }
24672
25020
  const sandboxes = await this.sandboxes.list();
24673
25021
  const existing = sandboxes.items.find((s) => s.name === nameOrId);
24674
25022
  if (existing) {
@@ -25774,7 +26122,7 @@ async function graphqlCommand(options) {
25774
26122
  "Ontology ID": payload.ontologyId,
25775
26123
  "Environment ID": payload.environmentId,
25776
26124
  "Session ID": payload.sessionId,
25777
- "OK": payload.ok ? "yes" : "no"
26125
+ OK: payload.ok ? "yes" : "no"
25778
26126
  });
25779
26127
  console.log();
25780
26128
  console.log(JSON.stringify(result, null, 2));
@@ -25785,7 +26133,9 @@ async function graphqlCommand(options) {
25785
26133
 
25786
26134
  // src/cli/commands/effects.ts
25787
26135
  function sortEffects(effects2) {
25788
- return [...effects2].sort((left, right) => left.name.localeCompare(right.name));
26136
+ return [...effects2].sort(
26137
+ (left, right) => left.name.localeCompare(right.name)
26138
+ );
25789
26139
  }
25790
26140
  async function effectsListCommand(options) {
25791
26141
  const emitJson = options.json === true;
@@ -25896,7 +26246,10 @@ async function effectsDiffCommand(options) {
25896
26246
  // src/cli/commands/job.ts
25897
26247
  async function jobRunCommand(options) {
25898
26248
  const emitJson = options.json === true;
25899
- const code = readJobCodeFromOptions({ file: options.file, inline: options.inline });
26249
+ const code = readJobCodeFromOptions({
26250
+ file: options.file,
26251
+ inline: options.inline
26252
+ });
25900
26253
  if (!emitJson) {
25901
26254
  printHeader();
25902
26255
  }
@@ -25951,7 +26304,7 @@ async function jobRunCommand(options) {
25951
26304
  "Environment ID": payload.environmentId,
25952
26305
  "Session ID": payload.sessionId,
25953
26306
  "Job ID": payload.jobId,
25954
- "Status": payload.status
26307
+ Status: payload.status
25955
26308
  });
25956
26309
  console.log();
25957
26310
  console.log(JSON.stringify(result, null, 2));
@@ -26100,7 +26453,9 @@ async function versionCurrentCommand(options) {
26100
26453
  ]);
26101
26454
  const current = resolveCurrentVersion(builds, tags);
26102
26455
  if (!current) {
26103
- throw new Error("No versions exist for this ontology yet. Run `granular build` first.");
26456
+ throw new Error(
26457
+ "No versions exist for this ontology yet. Run `granular build` first."
26458
+ );
26104
26459
  }
26105
26460
  const payload = {
26106
26461
  ontologyId: config.sandboxId,
@@ -26120,9 +26475,12 @@ async function versionCurrentCommand(options) {
26120
26475
  console.log();
26121
26476
  keyValue({
26122
26477
  "Ontology ID": payload.ontologyId,
26123
- "Current version": formatVersionLabel(payload.versionId, payload.versionNumber),
26124
- "Status": payload.status,
26125
- "Created": payload.createdAt,
26478
+ "Current version": formatVersionLabel(
26479
+ payload.versionId,
26480
+ payload.versionNumber
26481
+ ),
26482
+ Status: payload.status,
26483
+ Created: payload.createdAt,
26126
26484
  "Dev tag": payload.devVersionId || "not set",
26127
26485
  "Prod tag": payload.prodVersionId || "not set"
26128
26486
  });
@@ -26164,7 +26522,7 @@ async function versionDiffCommand(versionId, againstVersionId, options) {
26164
26522
  spin?.succeed(" Version diff ready.");
26165
26523
  console.log();
26166
26524
  keyValue({
26167
- "Version": formatVersionLabel(version2.buildId, version2.versionNumber),
26525
+ Version: formatVersionLabel(version2.buildId, version2.versionNumber),
26168
26526
  "Compared against": baseVersion ? formatVersionLabel(baseVersion.buildId, baseVersion.versionNumber) : diffResult.againstVersionId || "previous version",
26169
26527
  "Added operations": summary.added,
26170
26528
  "Changed operations": summary.changed,
@@ -26255,17 +26613,21 @@ async function tagMoveCommand(tagName, versionId) {
26255
26613
  spin.succeed(` ${tagName} now points to ${versionId}.`);
26256
26614
  console.log();
26257
26615
  keyValue({
26258
- "Tag": moved.name,
26616
+ Tag: moved.name,
26259
26617
  "Previous version": previousVersionId || "not set",
26260
26618
  "Current version": moved.targetVersionId || moved.targetBuildId || "not set",
26261
- "Protected": moved.protected ? "yes" : "no"
26619
+ Protected: moved.protected ? "yes" : "no"
26262
26620
  });
26263
26621
  console.log();
26264
26622
  if (moved.name === "prod") {
26265
- info("Existing prod environments keep their current version until they are transitioned.");
26623
+ info(
26624
+ "Existing prod environments keep their current version until they are transitioned."
26625
+ );
26266
26626
  console.log();
26267
26627
  } else if (moved.name === "dev") {
26268
- info("dev is usually automatic. You only need to move it manually for exceptional release workflows.");
26628
+ info(
26629
+ "dev is usually automatic. You only need to move it manually for exceptional release workflows."
26630
+ );
26269
26631
  console.log();
26270
26632
  }
26271
26633
  } catch (err) {