@granular-software/sdk 0.4.9 → 0.4.11

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
@@ -5388,6 +5388,7 @@ var import_dotenv = __toESM(require_main());
5388
5388
  // src/endpoints.ts
5389
5389
  var LOCAL_API_URL = "ws://localhost:8787/granular";
5390
5390
  var PRODUCTION_API_URL = "wss://cf-api-gateway.arthur6084.workers.dev/granular";
5391
+ var DEFAULT_LOCAL_API_KEY = "gn_sk_tenant_default_principal_local_e2e_00000000";
5391
5392
  var LOCAL_AUTH_URL = "http://localhost:3000";
5392
5393
  var PRODUCTION_AUTH_URL = "https://app.granular.software";
5393
5394
  function readEnv(name) {
@@ -5407,6 +5408,21 @@ function isTruthy(value) {
5407
5408
  const normalized = value.trim().toLowerCase();
5408
5409
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
5409
5410
  }
5411
+ function isLocalApiUrl(url) {
5412
+ try {
5413
+ const parsed = new URL(url);
5414
+ return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
5415
+ } catch {
5416
+ return false;
5417
+ }
5418
+ }
5419
+ function resolveAuthTokenForApiUrl(authToken, apiUrl) {
5420
+ if (!authToken.startsWith("sk_") || !isLocalApiUrl(apiUrl) || isTruthy(readEnv("GRANULAR_DISABLE_LOCAL_API_KEY_FALLBACK"))) {
5421
+ return authToken;
5422
+ }
5423
+ const override = readEnv("GRANULAR_LOCAL_API_KEY")?.trim();
5424
+ return override || DEFAULT_LOCAL_API_KEY;
5425
+ }
5410
5426
  function resolveEndpointMode(explicitMode) {
5411
5427
  const explicit = normalizeMode(explicitMode);
5412
5428
  if (explicit === "local" || explicit === "production") {
@@ -6762,7 +6778,7 @@ var ApiClient = class {
6762
6778
  apiKey;
6763
6779
  baseUrl;
6764
6780
  constructor(apiKey, apiUrl) {
6765
- this.apiKey = apiKey;
6781
+ this.apiKey = resolveAuthTokenForApiUrl(apiKey, apiUrl);
6766
6782
  this.baseUrl = apiUrl.replace("wss://", "https://").replace("ws://", "http://").replace(/\/ws$/, "");
6767
6783
  }
6768
6784
  async request(path4, options = {}) {
@@ -8660,6 +8676,816 @@ function buildStatus(status) {
8660
8676
  }
8661
8677
  }
8662
8678
 
8679
+ // src/cli/agent-docs/parse-manifest.ts
8680
+ function cardinalityLabel(rel) {
8681
+ const a = rel.leftIsMany ? rel.rightIsMany ? "Many-to-Many" : "One-to-Many" : rel.rightIsMany ? "Many-to-One" : "One-to-One";
8682
+ return a;
8683
+ }
8684
+ function parseManifestContent(manifest) {
8685
+ const classes = [];
8686
+ const relationships = [];
8687
+ const effects = [];
8688
+ for (const vol of manifest.volumes ?? []) {
8689
+ for (const op of vol.operations ?? []) {
8690
+ if (op.create && op.extends === "@std/class") {
8691
+ classes.push({
8692
+ name: op.create,
8693
+ fields: op.has || {},
8694
+ description: op.description
8695
+ });
8696
+ }
8697
+ if (op.defineRelationship) {
8698
+ const rel = op.defineRelationship;
8699
+ relationships.push({
8700
+ left: rel.left,
8701
+ right: rel.right,
8702
+ leftSubmodel: rel.leftSubmodel,
8703
+ rightSubmodel: rel.rightSubmodel,
8704
+ leftIsMany: rel.leftIsMany,
8705
+ rightIsMany: rel.rightIsMany,
8706
+ cardinalityLabel: cardinalityLabel(rel)
8707
+ });
8708
+ }
8709
+ if (op.withEffect) {
8710
+ const w = op.withEffect;
8711
+ effects.push({
8712
+ name: w.name,
8713
+ description: w.description,
8714
+ attachedClass: w.attachedClass,
8715
+ isStatic: w.isStatic,
8716
+ inputSchema: w.inputSchema,
8717
+ outputSchema: w.outputSchema
8718
+ });
8719
+ }
8720
+ }
8721
+ }
8722
+ return { classes, relationships, effects };
8723
+ }
8724
+ function relationshipEdgesForClass(className, rels) {
8725
+ const edges = [];
8726
+ for (const r of rels) {
8727
+ if (r.left === className) {
8728
+ edges.push({
8729
+ submodelKey: r.leftSubmodel,
8730
+ foreignClass: r.right,
8731
+ isMany: r.leftIsMany
8732
+ });
8733
+ }
8734
+ if (r.right === className) {
8735
+ edges.push({
8736
+ submodelKey: r.rightSubmodel,
8737
+ foreignClass: r.left,
8738
+ isMany: r.rightIsMany
8739
+ });
8740
+ }
8741
+ }
8742
+ return edges;
8743
+ }
8744
+ function toPascalCase(name) {
8745
+ return name.charAt(0).toUpperCase() + name.slice(1);
8746
+ }
8747
+
8748
+ // src/cli/agent-docs/concept-blocks.ts
8749
+ function manifestGuideGranularProductSection() {
8750
+ return `## What is Granular?
8751
+
8752
+ **Granular** is a **hosted service** plus **\`@granular-software/sdk\`**. Together they let you:
8753
+
8754
+ 1. **Declare** a **domain** (classes, relationships between entities, and **effects** \u2014 named actions that run in **your** backend).
8755
+ 2. **Build** that declaration from \`granular.json\` so the service compiles types and tooling for a **sandbox** (a workspace identified by \`sbx_\u2026\`).
8756
+ 3. **Store** **records** (instances of your classes) and **run jobs** (code executed in Granular\u2019s **sandbox runtime**) that call generated helpers and effects.
8757
+
8758
+ **Split of responsibility:** Data and domain rules live in Granular; **secrets, payment providers, email, and arbitrary HTTP** stay in **your** server via **effect handlers** you register with \`registerEffects\`.
8759
+
8760
+ ---
8761
+
8762
+ ## Which document to use
8763
+
8764
+ | Need | Open |
8765
+ |------|------|
8766
+ | **Understand Granular** (product, glossary, how to edit \`granular.json\`, validate with **build**, SDK + CLI reference) | **This file** \u2014 [docs/granular-manifest.md](granular-manifest.md) |
8767
+ | **This repository\u2019s ontology only** (exact class names, relationship keys, effect schemas, snippets for **this** \`sbx_\u2026\`) | [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md) |
8768
+
8769
+ Edit the domain in **git** (\`granular.json\`); the **sandbox** on the service holds the **built** domain and **data**.`;
8770
+ }
8771
+ function manifestGuideMainConceptsSection() {
8772
+ return `## Main concepts
8773
+
8774
+ The rest of this guide assumes these terms.
8775
+
8776
+ | Term | What it means |
8777
+ |------|----------------|
8778
+ | **Sandbox** | A **workspace** on Granular (\`sbx_\u2026\`). It stores the **compiled domain** and **your records** for one project/environment. Not a browser sandbox \u2014 think \u201Ctenant + schema + data\u201D for this app. |
8779
+ | **Domain** | The **ontology**: **classes**, **relationships**, and **effects**. Declared in \`granular.json\`, installed on the sandbox by **build**. Same idea as *schema* / *model*. |
8780
+ | **Manifest** | \`granular.json\` (definition under \`manifest\`). **Source** in the repo until you **build**. |
8781
+ | **Build** | \`granular build\` \u2014 uploads and compiles the manifest. Primary **validation**; fixes errors before relying on types. |
8782
+ | **Environment** | Return value of \`granular.connect({ sandbox, \u2026 })\`. Your **server-side** handle for \`recordObject\`, \`submitJob\`, \`graphql\`, etc., bound to one sandbox. |
8783
+ | **Session** | SDK base type; \`Environment\` **extends** \`Session\`. Job/prompt APIs live on \`Session\`; you still use the **\`Environment\`** instance from \`connect()\` in normal apps. |
8784
+ | **Job** | Code string passed to \`environment.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to \`./sandbox-tools\` (generated classes + effect entrypoints). |
8785
+ | **Effect** | Declared with \`withEffect\`; **handler** registered with \`registerEffects\` in **your** process. Jobs invoke effects; handlers do IO outside Granular. |
8786
+ | **Class** | Entity **kind** in the domain (e.g. \`book\`) with \`has\` fields in the manifest. |
8787
+ | **Record / object** | One **instance**: \`className\`, \`id\`, \`fields\`, \`relationships\`. Upsert with \`recordObject\`. |
8788
+ | **Relationship** | Declared link between two classes; defines **property names** and cardinality. Keys appear in \`recordObject({ relationships })\`. |
8789
+
8790
+ ### Data vs actions (short)
8791
+
8792
+ - **In-graph:** classes, fields, instances, relationships \u2014 manipulated with \`recordObject\` and job APIs on generated classes.
8793
+ - **Outside declared data:** **effects** \u2014 declared in the manifest, implemented on your server, called from jobs like functions.
8794
+
8795
+ There is no separate \`granular validate\`: **\`granular build\`** (and its errors) is how you catch manifest mistakes.`;
8796
+ }
8797
+ function manifestGuideEndToEndSection() {
8798
+ return `## From manifest to running app
8799
+
8800
+ | Step | Action | Outcome |
8801
+ |------|--------|---------|
8802
+ | 1 | Edit \`granular.json\` (\`manifest\` \u2192 \`volumes\` \u2192 \`operations\`) | Domain **source** in the repo |
8803
+ | 2 | \`granular build\` | Manifest **uploaded**; domain **compiled** for the configured sandbox; errors surface here |
8804
+ | 3 | (Optional) Run your **effects host** \u2014 e.g. \`npx tsx granular-effects.ts\` \u2014 calling \`registerEffects(sandboxId, \u2026)\` | Effect **handlers** attached in **your** process |
8805
+ | 4 | \`new Granular({ apiKey })\` then \`connect({ sandbox, userId, permissions })\` | **\`Environment\`** for that sandbox |
8806
+ | 5 | \`recordObject\` / \`recordObjects\` | **Records** stored with correct fields and relationship keys |
8807
+ | 6 | \`submitJob(\`\u2026\`)\` with imports from \`./sandbox-tools\` | **Jobs** run; may call **effects** \u2192 your handlers execute and return results |
8808
+
8809
+ **Accuracy tip:** After changing the manifest, always **re-build** before assuming generated types, \`./sandbox-tools\` names, or effect signatures match the file on disk.`;
8810
+ }
8811
+ function sandboxDocMainConceptsSection() {
8812
+ return `## Main concepts (recap)
8813
+
8814
+ | Term | One line |
8815
+ |------|----------|
8816
+ | **Sandbox** | Workspace \`sbx_\u2026\` \u2014 **domain** + **data** for this project. |
8817
+ | **Domain / ontology** | Classes + relationships + effects \u2014 **built** from \`granular.json\`. |
8818
+ | **Manifest** | \`granular.json\` \u2014 **source**; edit here, then \`granular build\`. |
8819
+ | **Environment** | \`connect()\` result \u2014 **record**, **submitJob**, **graphql** for this sandbox. |
8820
+ | **Session** | Base type; \`Environment\` extends it (job APIs). |
8821
+ | **Job** | \`submitJob\` code using \`./sandbox-tools\`. |
8822
+ | **Effect** | Declared in manifest; **handler** in your process. |
8823
+
8824
+ **Full product + manifest how-to:** [docs/granular-manifest.md](docs/granular-manifest.md).`;
8825
+ }
8826
+ function sandboxDocScopeSection(projectName) {
8827
+ return `## How this doc fits with the manifest guide
8828
+
8829
+ | Document | Role |
8830
+ |----------|------|
8831
+ | [docs/granular-manifest.md](docs/granular-manifest.md) | **Product + how-to:** glossary, \`granular.json\` shape, \`create\` / \`defineRelationship\` / \`withEffect\`, \`granular build\`, SDK + CLI. Use it to **define an accurate manifest** and understand **environment** vs **sandbox**. |
8832
+ | **This file** (\`GRANULAR_SANDBOX.md\`, project **${projectName}**) | **This sandbox only:** ids, **exact** names and keys, effect JSON schemas, copy-paste snippets \u2014 so you can **build** and **call** the API without guessing strings. |
8833
+
8834
+ If **syntax** or **Granular behavior** is unclear, use the manifest guide; use **this** file for **ontology facts** for **${projectName}**.`;
8835
+ }
8836
+
8837
+ // src/cli/agent-docs/manifest-guide.ts
8838
+ function generateManifestAgentGuide(options) {
8839
+ const { projectName } = options;
8840
+ return `# Granular manifest guide (for coding agents)
8841
+
8842
+ This guide is for **coding agents** who may have **no prior context on Granular**. It explains the product, core terms, how to **author** \`granular.json\`, **build** the domain, and use the SDK (\`connect\` \u2192 **environment**, \`recordObject\`, \`submitJob\`, **effects**). For **this repository\u2019s** exact class names, relationship keys, and effect payloads only, see [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md).
8843
+
8844
+ **Precedence:** User instructions in chat override this file. When in doubt, read \`granular.json\` and [AGENTS.md](../AGENTS.md).
8845
+
8846
+ ---
8847
+
8848
+ ${manifestGuideGranularProductSection()}
8849
+
8850
+ ---
8851
+
8852
+ ${manifestGuideMainConceptsSection()}
8853
+
8854
+ ---
8855
+
8856
+ ${manifestGuideEndToEndSection()}
8857
+
8858
+ ---
8859
+
8860
+ ## \`granular.json\` file shape (read this first)
8861
+
8862
+ The CLI stores the manifest inside a wrapper object. The file must look like:
8863
+
8864
+ \`\`\`json
8865
+ {
8866
+ "manifest": {
8867
+ "schemaVersion": 2,
8868
+ "name": "${projectName}",
8869
+ "description": "\u2026",
8870
+ "volumes": [ \u2026 ]
8871
+ }
8872
+ }
8873
+ \`\`\`
8874
+
8875
+ - **Do not** put \`schemaVersion\` / \`volumes\` at the top level without the \`manifest\` key \u2014 that is invalid for this CLI.
8876
+ - All schema work happens under \`manifest\`.
8877
+
8878
+ Project display name in this file: **${projectName}**.
8879
+
8880
+ ---
8881
+
8882
+ ## Manifest structure (\`schemaVersion: 2\`)
8883
+
8884
+ \`\`\`json
8885
+ {
8886
+ "schemaVersion": 2,
8887
+ "name": "my-app",
8888
+ "description": "optional string",
8889
+ "volumes": [
8890
+ {
8891
+ "name": "schema",
8892
+ "scope": "sandbox",
8893
+ "imports": [
8894
+ { "alias": "@std", "name": "standard_modules", "label": "prod" }
8895
+ ],
8896
+ "operations": [ ]
8897
+ }
8898
+ ]
8899
+ }
8900
+ \`\`\`
8901
+
8902
+ | Field | Meaning |
8903
+ |-------|---------|
8904
+ | \`volumes\` | Logical groups of operations. Starters use one volume named \`schema\`. |
8905
+ | \`scope\` | \`sandbox\` (typical), \`build\`, or \`user\` \u2014 where the volume applies. |
8906
+ | \`imports\` | Brings in \`standard_modules\` under alias \`@std\` so you can \`"extends": "@std/class"\`. |
8907
+ | \`imports[].label\` | Version label (e.g. \`prod\`) for the module. |
8908
+ | \`operations\` | Ordered list of **operations** (see below). |
8909
+
8910
+ ---
8911
+
8912
+ ## Operations: create a class (fields)
8913
+
8914
+ Domain classes **must** extend the standard class prototype:
8915
+
8916
+ \`\`\`json
8917
+ {
8918
+ "create": "invoice",
8919
+ "extends": "@std/class",
8920
+ "has": {
8921
+ "amount_cents": { "type": "number", "description": "Amount in minor units" },
8922
+ "status": { "type": "string", "description": "Workflow status" }
8923
+ }
8924
+ }
8925
+ \`\`\`
8926
+
8927
+ ### Field entries (\`has\`): precise rules
8928
+
8929
+ Each key under \`has\` is a **field name** on the class. Each value is a **field spec**. Getting this wrong breaks types and generated tooling.
8930
+
8931
+ | Property | Required? | Purpose |
8932
+ |----------|-----------|---------|
8933
+ | \`type\` | **Yes** for normal scalar fields | \`string\`, \`number\`, \`boolean\`, \`email\`, \`url\`, \`date\`, etc. |
8934
+ | \`description\` | Strongly recommended | Used in docs and UX. |
8935
+ | \`value\` | Optional | **Default** baked into the model (static metadata), not per-record data. Rare for entity fields. |
8936
+ | \`ref\` | Optional | Advanced reference. Prefer \`defineRelationship\` for links between entities. |
8937
+ | \`has\` | Optional | Nested structure (advanced). |
8938
+
8939
+ **Precision checklist for fields**
8940
+
8941
+ - Use **snake_case** consistently across manifest, \`recordObject\`, and sandbox code.
8942
+ - Every scalar field should have \`"type": "..."\`.
8943
+ - Do **not** store links to other entities as opaque strings when you can use **relationships** (next section).
8944
+
8945
+ ---
8946
+
8947
+ ## Operations: \`defineRelationship\` (model links precisely)
8948
+
8949
+ Relationships connect two classes and fix **property names** and **cardinality** on each side.
8950
+
8951
+ Example (customer \u2194 orders: one customer, many orders; each order has one customer):
8952
+
8953
+ \`\`\`json
8954
+ {
8955
+ "defineRelationship": {
8956
+ "left": "customer",
8957
+ "right": "order",
8958
+ "leftSubmodel": "orders",
8959
+ "rightSubmodel": "customer",
8960
+ "leftIsMany": true,
8961
+ "rightIsMany": false
8962
+ }
8963
+ }
8964
+ \`\`\`
8965
+
8966
+ ### How to read one \`defineRelationship\` block
8967
+
8968
+ | JSON field | Meaning |
8969
+ |------------|---------|
8970
+ | \`left\`, \`right\` | **Class names** (same strings as in \`"create": "customer"\`). |
8971
+ | \`leftSubmodel\` | On an instance of \`left\`, the **property name** pointing toward \`right\` (e.g. \`customer.orders\`). |
8972
+ | \`rightSubmodel\` | On an instance of \`right\`, the **property name** pointing back to \`left\` (e.g. \`order.customer\`). |
8973
+ | \`leftIsMany\` | If \`true\`, when recording a \`left\` instance, this side is a **list** of target ids. |
8974
+ | \`rightIsMany\` | Same for the \`right\` side. |
8975
+
8976
+ ### Mapping to \`recordObject({ relationships })\`
8977
+
8978
+ **Keys** in \`relationships\` are \`leftSubmodel\` or \`rightSubmodel\` depending on which **class** you are recording:
8979
+
8980
+ - Recording a **left** class instance \u2192 use \`leftSubmodel\` as the key (values are \`right\` class ids).
8981
+ - Recording a **right** class instance \u2192 use \`rightSubmodel\` as the key (values are \`left\` class ids).
8982
+
8983
+ **Values** are always **ids of the target objects** (the foreign class), as plain strings:
8984
+
8985
+ - **Many** side \u2192 \`string[]\`.
8986
+ - **One** side \u2192 a single \`string\`.
8987
+
8988
+ Example:
8989
+
8990
+ - \`recordObject({ className: 'customer', id: 'acme', relationships: { orders: ['ord_1', 'ord_2'] } })\`
8991
+ - \`recordObject({ className: 'order', id: 'ord_1', relationships: { customer: 'acme' } })\`
8992
+
8993
+ **Anti-patterns**
8994
+
8995
+ - Storing \`customer_id\` as a raw string field on \`order\` **without** a \`defineRelationship\` \u2014 you lose cardinality and typed navigation in the sandbox.
8996
+ - Putting **synthetic path strings** (e.g. class name + underscore + id) in \`relationships\` \u2014 pass **only** the other object\u2019s \`id\` for its class (e.g. \`ord_1\`), not encoded paths.
8997
+
8998
+ ---
8999
+
9000
+ ## Operations: \`withEffect\` (declare actions outside the sandbox)
9001
+
9002
+ **An effect** is a named, schema-backed **action** that sandbox code can call like a function. The **declaration** is in the manifest; the **implementation** runs in **your** server (API keys, databases, external APIs live there\u2014not inside the sandbox).
9003
+
9004
+ \`\`\`json
9005
+ {
9006
+ "withEffect": {
9007
+ "name": "charge_card",
9008
+ "description": "Charge the customer",
9009
+ "attachedClass": "invoice",
9010
+ "isStatic": false,
9011
+ "inputSchema": { "type": "object", "properties": { "amount_cents": { "type": "number" } }, "required": ["amount_cents"] },
9012
+ "outputSchema": { "type": "object", "properties": { "chargeId": { "type": "string" } }, "required": ["chargeId"] },
9013
+ "stability": "stable",
9014
+ "tags": ["billing"]
9015
+ }
9016
+ }
9017
+ \`\`\`
9018
+
9019
+ | Field | Meaning |
9020
+ |-------|---------|
9021
+ | \`name\` | Must match the \`name\` in \`registerEffects\`. |
9022
+ | \`attachedClass\` | Omit for **global** effects. Set for class-bound effects. |
9023
+ | \`isStatic\` | \`true\` \u2192 static method; \`false\` or omit \u2192 **instance** method (handler receives the object **id** first). |
9024
+ | \`inputSchema\` / \`outputSchema\` | JSON Schema; used for codegen and validation. |
9025
+
9026
+ **Lifecycle:** declare in manifest \u2192 \`granular build\` \u2192 implement handlers \u2192 \`granular.registerEffects(sandboxId, [...])\` \u2192 jobs call generated methods \u2192 your handler runs and returns the result to the job.
9027
+
9028
+ **Permissions:** \`connect({ permissions: [...] })\` assigns profiles (e.g. \`default\`) that can allow or deny which effects a user may call.
9029
+
9030
+ ---
9031
+
9032
+ ## IDs and labels
9033
+
9034
+ | Concept | Rule |
9035
+ |---------|------|
9036
+ | \`id\` in \`recordObject\` | **Per-class** unique string you choose. Two different classes *may* reuse the same id string. |
9037
+ | \`label\` | Display label; optional. |
9038
+ | \`recordObject\` | Upserts by \`{ className, id }\`; fields and relationships are reapplied. |
9039
+ | \`relationships\` values | Use the **target object\u2019s id** for its class (see above). |
9040
+
9041
+ ---
9042
+
9043
+ ## \`@granular-software/sdk\` \u2014 capability map (after build)
9044
+
9045
+ ### \`Granular\` client
9046
+
9047
+ | API | Purpose |
9048
+ |-----|---------|
9049
+ | \`new Granular({ apiKey, apiUrl?, endpointMode?, token?, tokenProvider?, \u2026 })\` | Auth. Env: \`GRANULAR_API_KEY\`, \`GRANULAR_API_URL\`, \`GRANULAR_ENDPOINT_MODE\`. |
9050
+ | \`recordUser({ userId, name?, email?, permissions? })\` | Upsert user for later \`connect\`. |
9051
+ | \`connect({ sandbox, userId?, granularId?, user?, permissions?, clientId?, initialHeap? })\` | Opens a session \u2192 **\`Environment\`**. |
9052
+ | \`registerEffects(sandboxId, effects)\` / \`registerEffect\` | Register handlers for manifest \`withEffect\` declarations. |
9053
+ | \`unregisterEffect\` / \`unregisterAllEffects\` / \`disconnectEffects\` | Stop effect handlers for a sandbox. |
9054
+ | \`granular.sandboxes\` | \`.list()\`, \`.get\`, \`.create\`, \`.update\`, \`.delete\` |
9055
+ | \`granular.permissionProfiles\` | Permission profiles per sandbox. |
9056
+ | \`granular.environments\` | List/create/delete environments (usually use \`connect()\`). |
9057
+ | \`granular.subjects\` | Subjects / assignments (see typings). |
9058
+
9059
+ ### \`Environment\` (connected session)
9060
+
9061
+ | API | Purpose |
9062
+ |-----|---------|
9063
+ | \`environmentId\`, \`sandboxId\`, \`apiEndpoint\`, \u2026 | Session context. |
9064
+ | \`applyManifest(manifest)\` | Apply manifest operations at runtime (alternative to CLI build for dynamic ontologies). |
9065
+ | \`recordObject\` / \`recordObjects\` | Upsert instances and relationships. |
9066
+ | \`enqueueRecordImport\`, \`listRecordImports\`, \`getRecordImportSummary\`, \u2026 | Background bulk import. |
9067
+ | \`graphql(query, variables?)\` | **GraphQL** \u2014 see dedicated subsection below. |
9068
+ | \`defineRelationship\`, \`getRelationships\`, \`attach\`, \`detach\`, \`listRelated\` | Imperative relationship operations (same ideas as manifest \`defineRelationship\`). |
9069
+ | \`submitJob(code)\` | Run code in the sandbox; import from \`./sandbox-tools\`. |
9070
+ | \`getDomain()\`, \`getDomainTypes()\`, \`getDomainDocs()\`, \`getDomainDocumentation()\` | Domain summary and generated TypeScript / docs. |
9071
+ | \`getEffects()\` / \`getTools()\`, \`onEffectsChanged()\` | Effect catalog and updates. |
9072
+ | \`checkReadiness()\`, \`on('readiness', \u2026)\` | Environment readiness. |
9073
+ | \`getHeap()\` | Session state snapshot (advanced). |
9074
+ | \`rpc(method, params)\` | Low-level session RPC (advanced). |
9075
+ | \`disconnect()\` | End the session. |
9076
+
9077
+ ### GraphQL API (\`env.graphql\`)
9078
+
9079
+ Use \`environment.graphql(query, variables?)\` when you need **query/mutation access to the underlying graph** (paths, models, relationships). This is optional for many apps; \`recordObject\` and generated classes cover the common case. Authenticated with your API key.
9080
+
9081
+ ### \`Session\` (base class)
9082
+
9083
+ | API | Purpose |
9084
+ |-----|---------|
9085
+ | \`submitJob\`, \`answerPrompt\`, job APIs | Jobs and human-in-the-loop prompts. |
9086
+
9087
+ ---
9088
+
9089
+ ## CLI commands
9090
+
9091
+ | Command | Purpose |
9092
+ |---------|---------|
9093
+ | \`granular build\` | Upload \`granular.json\` and compile (primary **validation**). |
9094
+ | \`granular deploy\` | Build + deploy current production. |
9095
+ | \`granular dev\` | Watch manifest, rebuild on change. |
9096
+ | \`granular document\` | Regenerate \`GRANULAR_SANDBOX.md\`. |
9097
+ | \`granular init\` | Scaffold project; optional \`--agent-docs\`. |
9098
+ | \`granular add class|field|relation\` | Mutate local manifest. |
9099
+ | \`granular simulate\` | Open simulator in browser. |
9100
+ | \`granular pull\` | Pull manifest from API. |
9101
+ | \`granular login\`, \`granular whoami\`, \`granular status\` | Auth / project info. |
9102
+
9103
+ ---
9104
+
9105
+ ## After you change the manifest
9106
+
9107
+ 1. \`granular build\` (or \`granular dev\`).
9108
+ 2. Align **effect handlers** with every \`withEffect\` name + schemas.
9109
+ 3. \`granular document\` to refresh \`GRANULAR_SANDBOX.md\` without a full build if needed (build still records manifest/build ids).
9110
+
9111
+ ---
9112
+
9113
+ ## Further reading
9114
+
9115
+ - [docs.granular.dev](https://docs.granular.dev)
9116
+ - Generated sandbox snapshot: [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md)
9117
+ `;
9118
+ }
9119
+
9120
+ // src/cli/agent-docs/agents-md.ts
9121
+ var GRANULAR_AGENTS_BEGIN = "<!-- granular-sdk:begin -->";
9122
+ var GRANULAR_AGENTS_END = "<!-- granular-sdk:end -->";
9123
+ function generateGranularAgentsBlock(options) {
9124
+ const manifestPath = options?.manifestGuidePath ?? "docs/granular-manifest.md";
9125
+ return `${GRANULAR_AGENTS_BEGIN}
9126
+
9127
+ ## Granular (this project)
9128
+
9129
+ **No prior Granular context assumed.** Read in order:
9130
+
9131
+ 1. **[${manifestPath}](${manifestPath})** \u2014 What Granular is, glossary, \`granular.json\` operations, \`granular build\`, \`connect\` / **environment** / **session**, \`submitJob\`, \`recordObject\`, **effects**, SDK + CLI reference.
9132
+ 2. **[GRANULAR_SANDBOX.md](GRANULAR_SANDBOX.md)** \u2014 This repo\u2019s **ontology only**: exact class/field names, relationship keys, effect schemas, snippets for **this** sandbox id.
9133
+
9134
+ **Instruction precedence:** User chat > this section > other content in this file.
9135
+
9136
+ ${GRANULAR_AGENTS_END}`;
9137
+ }
9138
+ function mergeGranularAgentsBlock(existingContent, block) {
9139
+ const trimmed = existingContent.trimEnd();
9140
+ if (trimmed.includes(GRANULAR_AGENTS_BEGIN) && trimmed.includes(GRANULAR_AGENTS_END)) {
9141
+ const re = new RegExp(
9142
+ `${escapeRegex(GRANULAR_AGENTS_BEGIN)}[\\s\\S]*?${escapeRegex(GRANULAR_AGENTS_END)}`,
9143
+ "m"
9144
+ );
9145
+ return existingContent.replace(re, block.trim());
9146
+ }
9147
+ if (!trimmed) {
9148
+ return `${block.trim()}
9149
+ `;
9150
+ }
9151
+ return `${trimmed}
9152
+
9153
+ ${block.trim()}
9154
+ `;
9155
+ }
9156
+ function escapeRegex(s) {
9157
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9158
+ }
9159
+
9160
+ // src/cli/agent-docs/sandbox-doc.ts
9161
+ function exampleScalarValue(type) {
9162
+ if (type === "number") return 0;
9163
+ if (type === "boolean") return true;
9164
+ if (type === "date") return "2026-01-01";
9165
+ if (type === "email") return "user@example.com";
9166
+ if (type === "url") return "https://example.com";
9167
+ return "example";
9168
+ }
9169
+ function formatSchemaSnippet(obj, maxLen = 2e3) {
9170
+ const s = JSON.stringify(obj, null, 2);
9171
+ if (s.length <= maxLen) return s;
9172
+ return `${s.slice(0, maxLen)}
9173
+ /* \u2026 truncated \u2026 */`;
9174
+ }
9175
+ function generateSandboxAgentDoc(manifest, meta) {
9176
+ const parsed = parseManifestContent(manifest);
9177
+ const { classes, relationships, effects } = parsed;
9178
+ const lines = [];
9179
+ const seed = meta.seedScriptName ?? "granular-seed.ts";
9180
+ const eff = meta.effectsScriptName ?? "granular-effects.ts";
9181
+ lines.push(`# ${manifest.name} \u2014 sandbox agent reference`);
9182
+ lines.push("");
9183
+ lines.push(
9184
+ "> **For coding agents:** **Project-specific** snapshot (this sandbox\u2019s classes, keys, effects). For what Granular is and how manifests work, read [docs/granular-manifest.md](docs/granular-manifest.md) first. Regenerate with `granular document` or after `granular build`."
9185
+ );
9186
+ lines.push("");
9187
+ lines.push("## Sandbox identity");
9188
+ lines.push("");
9189
+ lines.push("| Key | Value |");
9190
+ lines.push("|-----|-------|");
9191
+ lines.push(`| Sandbox id | \`${meta.sandboxId}\` |`);
9192
+ if (meta.apiUrl) lines.push(`| API / WS base | \`${meta.apiUrl}\` |`);
9193
+ if (meta.manifestId) lines.push(`| Last uploaded manifest id | \`${meta.manifestId}\` |`);
9194
+ if (meta.buildId) lines.push(`| Last build id | \`${meta.buildId}\` |`);
9195
+ if (meta.buildPending) {
9196
+ lines.push("| Build status | *No successful build recorded in this doc run \u2014 run `granular build`.* |");
9197
+ }
9198
+ lines.push("");
9199
+ lines.push("Set `GRANULAR_API_KEY` (e.g. in `.env.local`). Optional: `GRANULAR_API_URL` overrides the WebSocket URL.");
9200
+ lines.push("");
9201
+ for (const line of sandboxDocMainConceptsSection().split("\n")) {
9202
+ lines.push(line);
9203
+ }
9204
+ lines.push("");
9205
+ for (const line of sandboxDocScopeSection(manifest.name).split("\n")) {
9206
+ lines.push(line);
9207
+ }
9208
+ lines.push("");
9209
+ lines.push("## Quick CLI");
9210
+ lines.push("");
9211
+ lines.push("| Command | Purpose |");
9212
+ lines.push("|---------|---------|");
9213
+ lines.push("| `granular build` | Upload manifest and build |");
9214
+ lines.push("| `granular document` | Refresh this file from `granular.json` |");
9215
+ lines.push("| `npx tsx " + seed + "` | Seed sample records (if present) |");
9216
+ lines.push("| `npx tsx " + eff + "` | Run effect handler host (if present) |");
9217
+ lines.push("");
9218
+ lines.push("## Connect");
9219
+ lines.push("");
9220
+ lines.push("```typescript");
9221
+ lines.push(`import { Granular } from '@granular-software/sdk';`);
9222
+ lines.push("");
9223
+ lines.push(`const granular = new Granular({`);
9224
+ lines.push(` apiKey: process.env.GRANULAR_API_KEY!,`);
9225
+ lines.push(` // apiUrl: process.env.GRANULAR_API_URL, // optional`);
9226
+ lines.push(`});`);
9227
+ lines.push("");
9228
+ lines.push(`const env = await granular.connect({`);
9229
+ lines.push(` sandbox: '${meta.sandboxId}',`);
9230
+ lines.push(` userId: 'your_app_user_id',`);
9231
+ lines.push(` permissions: ['default'],`);
9232
+ lines.push(`});`);
9233
+ lines.push("```");
9234
+ lines.push("");
9235
+ lines.push("## Classes and fields");
9236
+ lines.push("");
9237
+ if (classes.length === 0) {
9238
+ lines.push("*No `create` + `@std/class` operations found in the manifest.*");
9239
+ lines.push("");
9240
+ } else {
9241
+ for (const cls of classes) {
9242
+ lines.push(`### \`${cls.name}\``);
9243
+ lines.push("");
9244
+ if (cls.description) lines.push(cls.description);
9245
+ lines.push("");
9246
+ const keys = Object.keys(cls.fields);
9247
+ if (keys.length === 0) {
9248
+ lines.push("*No scalar fields in `has`.*");
9249
+ } else {
9250
+ lines.push("| Field | Type | Description |");
9251
+ lines.push("|-------|------|-------------|");
9252
+ for (const k of keys) {
9253
+ const spec = cls.fields[k];
9254
+ const t = spec?.type || "\u2014";
9255
+ const d = spec?.description || "\u2014";
9256
+ lines.push(`| \`${k}\` | \`${String(t)}\` | ${d} |`);
9257
+ }
9258
+ }
9259
+ lines.push("");
9260
+ }
9261
+ }
9262
+ lines.push("## Relationships");
9263
+ lines.push("");
9264
+ lines.push("Declared with `defineRelationship`. Submodel names are the **keys** on each class for `recordObject({ relationships: { ... } })`. Values are **foreign class ids** (single string or `string[]` when \u201Cmany\u201D).");
9265
+ lines.push("");
9266
+ if (relationships.length === 0) {
9267
+ lines.push("*No relationships defined.*");
9268
+ lines.push("");
9269
+ } else {
9270
+ lines.push("| Left | Right | Cardinality | Left key (`leftSubmodel`) | Right key (`rightSubmodel`) |");
9271
+ lines.push("|------|-------|-------------|---------------------------|----------------------------|");
9272
+ for (const r of relationships) {
9273
+ lines.push(
9274
+ `| \`${r.left}\` | \`${r.right}\` | ${r.cardinalityLabel} | \`${r.leftSubmodel}\` | \`${r.rightSubmodel}\` |`
9275
+ );
9276
+ }
9277
+ lines.push("");
9278
+ }
9279
+ lines.push("## `recordObject` \u2014 examples per class");
9280
+ lines.push("");
9281
+ lines.push("`recordObject` upserts by `{ className, id }`. In `relationships`, use **only the target object\u2019s id** for its class (see keys below).");
9282
+ lines.push("");
9283
+ for (const cls of classes) {
9284
+ const edges = relationshipEdgesForClass(cls.name, relationships);
9285
+ lines.push(`### \`${cls.name}\``);
9286
+ lines.push("");
9287
+ lines.push("```typescript");
9288
+ lines.push(`await env.recordObject({`);
9289
+ lines.push(` className: '${cls.name}',`);
9290
+ lines.push(` id: 'your_${cls.name}_id',`);
9291
+ lines.push(` label: 'Label',`);
9292
+ const fieldKeys = Object.keys(cls.fields);
9293
+ if (fieldKeys.length > 0) {
9294
+ lines.push(` fields: {`);
9295
+ for (const k of fieldKeys) {
9296
+ const spec = cls.fields[k];
9297
+ const v = exampleScalarValue(spec?.type);
9298
+ const lit = typeof v === "string" ? `'${v}'` : JSON.stringify(v);
9299
+ lines.push(` ${k}: ${lit},`);
9300
+ }
9301
+ lines.push(` },`);
9302
+ }
9303
+ if (edges.length > 0) {
9304
+ lines.push(` relationships: {`);
9305
+ for (const e of edges) {
9306
+ if (e.isMany) {
9307
+ lines.push(` ${e.submodelKey}: ['${e.foreignClass}_id_1'], // ${e.foreignClass} ids`);
9308
+ } else {
9309
+ lines.push(` ${e.submodelKey}: '${e.foreignClass}_id', // one ${e.foreignClass} id`);
9310
+ }
9311
+ }
9312
+ lines.push(` },`);
9313
+ }
9314
+ lines.push(`});`);
9315
+ lines.push("```");
9316
+ lines.push("");
9317
+ }
9318
+ if (classes.length === 0) {
9319
+ lines.push("*No classes \u2014 no examples.*");
9320
+ lines.push("");
9321
+ }
9322
+ lines.push("## Effects declared in this manifest (`withEffect` + live handlers)");
9323
+ lines.push("");
9324
+ lines.push("Each entry below matches one `withEffect` operation. For every declaration you must:");
9325
+ lines.push("");
9326
+ lines.push("1. **Build** the manifest so the domain package includes the effect (`granular build`).");
9327
+ lines.push("2. **Register** a handler whose `name` (and `className` / `static` when applicable) matches \u2014 use `granular.registerEffects('" + meta.sandboxId + "', [...])` in your host process (see `granular-effects.ts` in starter projects).");
9328
+ lines.push("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.");
9329
+ lines.push("");
9330
+ lines.push("**Handler shapes:** global \u2192 `(params, ctx) => \u2026`; static class effect \u2192 `(params, ctx) => \u2026`; instance effect \u2192 `(objectId, params, ctx) => \u2026` where `objectId` is the real-world id for `attachedClass`.");
9331
+ lines.push("");
9332
+ if (effects.length === 0) {
9333
+ lines.push("*No `withEffect` operations in this manifest.*");
9334
+ lines.push("");
9335
+ } else {
9336
+ for (const ef of effects) {
9337
+ const scope = ef.attachedClass ? ef.isStatic ? `static on \`${ef.attachedClass}\`` : `instance on \`${ef.attachedClass}\`` : "global";
9338
+ lines.push(`### \`${ef.name}\` (${scope})`);
9339
+ lines.push("");
9340
+ if (ef.description) lines.push(ef.description);
9341
+ lines.push("");
9342
+ lines.push("**Input schema:**");
9343
+ lines.push("");
9344
+ lines.push("```json");
9345
+ lines.push(formatSchemaSnippet(ef.inputSchema));
9346
+ lines.push("```");
9347
+ lines.push("");
9348
+ if (ef.outputSchema) {
9349
+ lines.push("**Output schema:**");
9350
+ lines.push("");
9351
+ lines.push("```json");
9352
+ lines.push(formatSchemaSnippet(ef.outputSchema));
9353
+ lines.push("```");
9354
+ lines.push("");
9355
+ }
9356
+ lines.push("**Handler signature (TypeScript):**");
9357
+ lines.push("");
9358
+ if (!ef.attachedClass) {
9359
+ lines.push("```typescript");
9360
+ lines.push(`// Global: (params, ctx) => Promise<output>`);
9361
+ lines.push(`handler: async (params, ctx) => { /* ... */ }`);
9362
+ lines.push("```");
9363
+ } else if (ef.isStatic) {
9364
+ lines.push("```typescript");
9365
+ lines.push(`// Static: (params, ctx) => Promise<output>`);
9366
+ lines.push(`handler: async (params, ctx) => { /* ... */ }`);
9367
+ lines.push("```");
9368
+ } else {
9369
+ lines.push("```typescript");
9370
+ lines.push(`// Instance: (objectId, params, ctx) => Promise<output>`);
9371
+ lines.push(`handler: async (objectId, params, ctx) => { /* objectId is the ${ef.attachedClass} id */ }`);
9372
+ lines.push("```");
9373
+ }
9374
+ lines.push("");
9375
+ }
9376
+ }
9377
+ lines.push("## `submitJob` / sandbox code");
9378
+ lines.push("");
9379
+ lines.push("Jobs run in the sandbox with generated classes from `./sandbox-tools`. Class names are PascalCase.");
9380
+ lines.push("");
9381
+ const pascalImports = classes.map((c) => toPascalCase(c.name));
9382
+ const effectNames = effects.map((e) => e.name).filter(Boolean);
9383
+ const globalEffects = effects.filter((e) => !e.attachedClass);
9384
+ const firstGlobal = globalEffects[0]?.name;
9385
+ lines.push("```typescript");
9386
+ lines.push(`const job = await env.submitJob(\``);
9387
+ const importList = [...pascalImports, ...effectNames].filter((v, i, a) => a.indexOf(v) === i);
9388
+ if (importList.length > 0) {
9389
+ lines.push(` import { ${importList.join(", ")} } from './sandbox-tools';`);
9390
+ } else {
9391
+ lines.push(` // import generated classes/tools from './sandbox-tools'`);
9392
+ }
9393
+ lines.push("");
9394
+ if (classes.length > 0) {
9395
+ const classWithInstanceEffect = classes.find(
9396
+ (cl) => effects.some((e) => e.attachedClass === cl.name && !e.isStatic)
9397
+ );
9398
+ const target = classWithInstanceEffect ?? classes[0];
9399
+ const C = toPascalCase(target.name);
9400
+ const c = target.name;
9401
+ lines.push(` const rows = await ${C}.list({ limit: 10, saveAs: '${c}_rows' });`);
9402
+ lines.push(` const row = rows[0] ?? null;`);
9403
+ lines.push("");
9404
+ const instEffects = effects.filter((e) => e.attachedClass === c && !e.isStatic);
9405
+ if (instEffects.length > 0) {
9406
+ const n = instEffects[0].name;
9407
+ lines.push(` if (row) {`);
9408
+ lines.push(` const out = await row.${n}({ /* input per schema */ });`);
9409
+ lines.push(` console.log(out);`);
9410
+ lines.push(` }`);
9411
+ } else {
9412
+ lines.push(` console.log(row?.id);`);
9413
+ }
9414
+ lines.push("");
9415
+ }
9416
+ if (firstGlobal) {
9417
+ lines.push(` await ${firstGlobal}({ /* input per schema */ });`);
9418
+ lines.push("");
9419
+ }
9420
+ lines.push(` return { ok: true };`);
9421
+ lines.push(`\`);`);
9422
+ lines.push("");
9423
+ lines.push(`const result = await job.result;`);
9424
+ lines.push("```");
9425
+ lines.push("");
9426
+ lines.push("## Companion scripts");
9427
+ lines.push("");
9428
+ lines.push(`- **\`${seed}\`** \u2014 example \`recordObject\` batch for starter data.`);
9429
+ lines.push(`- **\`${eff}\`** \u2014 long-running process that registers effect handlers for this sandbox.`);
9430
+ lines.push("");
9431
+ lines.push("## SDK surface (reminder)");
9432
+ lines.push("");
9433
+ lines.push("| Area | APIs |");
9434
+ lines.push("|------|------|");
9435
+ lines.push("| Auth / session | `Granular`, `connect`, `disconnect`, `recordUser` |");
9436
+ lines.push("| Data | `recordObject`, `recordObjects`, record import queue APIs |");
9437
+ lines.push("| GraphQL API | `graphql` \u2014 query/mutate the underlying graph when you need it |");
9438
+ lines.push("| Relationships (API) | `defineRelationship`, `getRelationships`, `attach`, `detach`, `listRelated` |");
9439
+ lines.push("| Ontology | `applyManifest` (runtime), or CLI `granular build` |");
9440
+ lines.push("| Jobs | `submitJob`, `answerPrompt` |");
9441
+ lines.push("| Domain | `getDomain`, `getDomainTypes`, `getDomainDocumentation` |");
9442
+ lines.push("| Effects | `registerEffects`, `getEffects`, `onEffectsChanged` |");
9443
+ lines.push("| Ops | `checkReadiness`, `getHeap`, `rpc` |");
9444
+ lines.push("");
9445
+ lines.push("Full list: [docs/granular-manifest.md](docs/granular-manifest.md).");
9446
+ lines.push("");
9447
+ lines.push("---");
9448
+ lines.push("");
9449
+ lines.push(`*Generated at ${(/* @__PURE__ */ new Date()).toISOString()} by @granular-software/sdk*`);
9450
+ lines.push("");
9451
+ return lines.join("\n");
9452
+ }
9453
+ var MANIFEST_GUIDE_RELATIVE = path__namespace.join("docs", "granular-manifest.md");
9454
+ var SANDBOX_DOC_FILENAME = "GRANULAR_SANDBOX.md";
9455
+ function getAgentsMdPath(projectRoot) {
9456
+ return path__namespace.join(projectRoot, "AGENTS.md");
9457
+ }
9458
+ function getManifestGuidePath(projectRoot) {
9459
+ return path__namespace.join(projectRoot, MANIFEST_GUIDE_RELATIVE);
9460
+ }
9461
+ function getSandboxDocPath(projectRoot) {
9462
+ return path__namespace.join(projectRoot, SANDBOX_DOC_FILENAME);
9463
+ }
9464
+ function writeManifestAgentDocs(projectRoot, projectName) {
9465
+ const docsDir = path__namespace.join(projectRoot, "docs");
9466
+ if (!fs__namespace.existsSync(docsDir)) {
9467
+ fs__namespace.mkdirSync(docsDir, { recursive: true });
9468
+ }
9469
+ const guidePath = getManifestGuidePath(projectRoot);
9470
+ const guide = generateManifestAgentGuide({ projectName });
9471
+ fs__namespace.writeFileSync(guidePath, guide, "utf-8");
9472
+ const agentsPath = getAgentsMdPath(projectRoot);
9473
+ const block = generateGranularAgentsBlock({ manifestGuidePath: MANIFEST_GUIDE_RELATIVE.replace(/\\/g, "/") });
9474
+ const previous = fs__namespace.existsSync(agentsPath) ? fs__namespace.readFileSync(agentsPath, "utf-8") : "";
9475
+ const merged = mergeGranularAgentsBlock(previous, block);
9476
+ fs__namespace.writeFileSync(agentsPath, merged, "utf-8");
9477
+ }
9478
+ function writeSandboxAgentDocFile(projectRoot, manifest, meta) {
9479
+ const metaWithScripts = {
9480
+ ...meta,
9481
+ seedScriptName: meta.seedScriptName ?? SEED_SCRIPT_NAME,
9482
+ effectsScriptName: meta.effectsScriptName ?? EFFECTS_SCRIPT_NAME
9483
+ };
9484
+ const body = generateSandboxAgentDoc(manifest, metaWithScripts);
9485
+ const out = getSandboxDocPath(projectRoot);
9486
+ fs__namespace.writeFileSync(out, body, "utf-8");
9487
+ }
9488
+
8663
9489
  // src/cli/commands/init.ts
8664
9490
  function prompt(question, defaultValue) {
8665
9491
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
@@ -8726,6 +9552,10 @@ async function selectStarterTemplate(requestedTemplate) {
8726
9552
  }
8727
9553
  async function initCommand(projectName, options) {
8728
9554
  printHeader();
9555
+ if (options?.agentDocs && options?.noAgentDocs) {
9556
+ error("Cannot use --agent-docs and --no-agent-docs together.");
9557
+ process.exit(1);
9558
+ }
8729
9559
  if (manifestExists()) {
8730
9560
  warn("A granular.json already exists in this directory.");
8731
9561
  const overwrite = await confirm("Overwrite?", false);
@@ -8803,6 +9633,25 @@ async function initCommand(projectName, options) {
8803
9633
  success(`Created ${brand.bold(".granularrc")}`);
8804
9634
  ensureGitignore();
8805
9635
  success("Updated .gitignore");
9636
+ let wantAgentDocs = false;
9637
+ if (options?.agentDocs) {
9638
+ wantAgentDocs = true;
9639
+ } else if (options?.noAgentDocs) {
9640
+ wantAgentDocs = false;
9641
+ } else if (process.stdin.isTTY && process.stdout.isTTY) {
9642
+ console.log();
9643
+ wantAgentDocs = await confirm(
9644
+ "Add agent documentation (AGENTS.md + docs/granular-manifest.md for AI coding tools)?",
9645
+ true
9646
+ );
9647
+ }
9648
+ if (wantAgentDocs) {
9649
+ writeManifestAgentDocs(getProjectRoot(), name);
9650
+ success(`Created ${brand.bold("AGENTS.md")} and ${brand.bold("docs/granular-manifest.md")}`);
9651
+ }
9652
+ let lastManifestId;
9653
+ let lastBuildId;
9654
+ let initialBuildSucceeded = false;
8806
9655
  console.log();
8807
9656
  const shouldBuild = options?.skipBuild ? false : await confirm("Trigger initial build?", true);
8808
9657
  if (shouldBuild) {
@@ -8814,12 +9663,15 @@ async function initCommand(projectName, options) {
8814
9663
  project.manifest,
8815
9664
  "1.0.0"
8816
9665
  );
9666
+ lastManifestId = manifest.manifestId;
8817
9667
  building.text = " Triggering build...";
8818
9668
  const build = await api.triggerBuild(sandbox.sandboxId, manifest.manifestId);
8819
9669
  building.text = " Building...";
8820
9670
  const completed = await api.waitForBuild(build.buildId, (status) => {
8821
9671
  building.text = ` Building... ${brand.muted(status)}`;
8822
9672
  });
9673
+ lastBuildId = completed.buildId;
9674
+ initialBuildSucceeded = true;
8823
9675
  building.succeed(` Build completed: ${brand.secondary(completed.buildId)}`);
8824
9676
  } catch (err) {
8825
9677
  building.fail(` Build failed: ${err.message}`);
@@ -8840,20 +9692,34 @@ async function initCommand(projectName, options) {
8840
9692
  "utf-8"
8841
9693
  );
8842
9694
  success(`Created ${brand.bold(SEED_SCRIPT_NAME)} (sample record seeding script)`);
9695
+ writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
9696
+ sandboxId: sandbox.sandboxId,
9697
+ apiUrl,
9698
+ manifestId: lastManifestId,
9699
+ buildId: lastBuildId,
9700
+ buildPending: !initialBuildSucceeded
9701
+ });
9702
+ success(`Wrote ${brand.bold("GRANULAR_SANDBOX.md")} (agent reference for this sandbox)`);
8843
9703
  console.log();
8844
9704
  divider();
8845
9705
  console.log();
8846
9706
  success(`Project ${brand.bold(name)} initialized!`);
8847
9707
  console.log();
8848
- keyValue({
9708
+ const kv = {
8849
9709
  "Sandbox": sandbox.sandboxId,
8850
9710
  "Template": template.label,
8851
9711
  "Manifest": "granular.json",
8852
9712
  "Config": ".granularrc",
8853
9713
  "API Key": ".env.local",
8854
9714
  "Seed script": SEED_SCRIPT_NAME,
8855
- "Effects script": EFFECTS_SCRIPT_NAME
8856
- });
9715
+ "Effects script": EFFECTS_SCRIPT_NAME,
9716
+ "Sandbox agent doc": "GRANULAR_SANDBOX.md"
9717
+ };
9718
+ if (wantAgentDocs) {
9719
+ kv["Agent manifest guide"] = "docs/granular-manifest.md";
9720
+ kv["Agent index"] = "AGENTS.md";
9721
+ }
9722
+ keyValue(kv);
8857
9723
  const nextSteps2 = options?.skipBuild ? [
8858
9724
  { command: "granular build", description: "Build the starter ontology before you seed or simulate" },
8859
9725
  { command: `npx tsx ${SEED_SCRIPT_NAME}`, description: "Push the sample records into your sandbox" },
@@ -8872,7 +9738,7 @@ async function initCommand(projectName, options) {
8872
9738
  dim(" The seed script shows how records enter the graph. The effects script shows how external systems plug in.");
8873
9739
  console.log();
8874
9740
  }
8875
- var DEFAULT_LOCAL_API_KEY = "gn_sk_tenant_default_principal_local_e2e_00000000";
9741
+ var DEFAULT_LOCAL_API_KEY2 = "gn_sk_tenant_default_principal_local_e2e_00000000";
8876
9742
  function promptSecret(question) {
8877
9743
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
8878
9744
  return new Promise((resolve) => {
@@ -8898,7 +9764,7 @@ function maskApiKey(apiKey) {
8898
9764
  if (apiKey.length < 10) return `${apiKey}...`;
8899
9765
  return `${apiKey.substring(0, 10)}...`;
8900
9766
  }
8901
- function isLocalApiUrl(apiUrl) {
9767
+ function isLocalApiUrl2(apiUrl) {
8902
9768
  return apiUrl.startsWith("ws://localhost:") || apiUrl.startsWith("wss://localhost:") || apiUrl.startsWith("ws://127.0.0.1:") || apiUrl.startsWith("wss://127.0.0.1:") || apiUrl.startsWith("http://localhost:") || apiUrl.startsWith("https://localhost:") || apiUrl.startsWith("http://127.0.0.1:") || apiUrl.startsWith("https://127.0.0.1:");
8903
9769
  }
8904
9770
  async function loginCommand(options = {}) {
@@ -8912,8 +9778,8 @@ async function loginCommand(options = {}) {
8912
9778
  const apiUrl = loadApiUrl();
8913
9779
  let apiKey = options.apiKey?.trim();
8914
9780
  if (!apiKey) {
8915
- if (!options.manual && isLocalApiUrl(apiUrl)) {
8916
- apiKey = existing?.trim() || process.env.GRANULAR_LOCAL_API_KEY?.trim() || DEFAULT_LOCAL_API_KEY;
9781
+ if (!options.manual && isLocalApiUrl2(apiUrl)) {
9782
+ apiKey = existing?.trim() || process.env.GRANULAR_LOCAL_API_KEY?.trim() || DEFAULT_LOCAL_API_KEY2;
8917
9783
  info("Using local Granular API key for localhost development.");
8918
9784
  } else if (options.manual) {
8919
9785
  dim("Get your API key at https://app.granular.software/w/default/api-keys");
@@ -9040,12 +9906,19 @@ async function buildCommand() {
9040
9906
  });
9041
9907
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9042
9908
  building.succeed(` Build completed in ${totalTime}s`);
9909
+ writeSandboxAgentDocFile(getProjectRoot(), manifest, {
9910
+ sandboxId: config.sandboxId,
9911
+ apiUrl: config.apiUrl,
9912
+ manifestId: uploadedManifest.manifestId,
9913
+ buildId: completed.buildId
9914
+ });
9043
9915
  console.log();
9044
9916
  keyValue({
9045
9917
  "Build ID": completed.buildId,
9046
9918
  "Manifest": uploadedManifest.manifestId,
9047
9919
  "Status": "completed",
9048
- "Duration": `${totalTime}s`
9920
+ "Duration": `${totalTime}s`,
9921
+ "Agent doc": "GRANULAR_SANDBOX.md"
9049
9922
  });
9050
9923
  console.log();
9051
9924
  } catch (err) {
@@ -9063,9 +9936,9 @@ async function deployCommand() {
9063
9936
  process.exit(1);
9064
9937
  }
9065
9938
  const api = new ApiClient(config.apiKey, config.apiUrl);
9066
- const manifest = config.project.manifest;
9067
9939
  step("Deploy", `Sandbox ${config.sandboxId}`);
9068
9940
  console.log();
9941
+ const manifest = config.project.manifest;
9069
9942
  const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
9070
9943
  let uploadedManifest;
9071
9944
  try {
@@ -9085,13 +9958,20 @@ async function deployCommand() {
9085
9958
  });
9086
9959
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9087
9960
  building.succeed(` Build completed in ${totalTime}s`);
9961
+ writeSandboxAgentDocFile(getProjectRoot(), manifest, {
9962
+ sandboxId: config.sandboxId,
9963
+ apiUrl: config.apiUrl,
9964
+ manifestId: uploadedManifest.manifestId,
9965
+ buildId: completed.buildId
9966
+ });
9088
9967
  console.log();
9089
9968
  success(`Deployed ${brand.bold(manifest.name)} successfully!`);
9090
9969
  console.log();
9091
9970
  keyValue({
9092
9971
  "Build": completed.buildId,
9093
9972
  "Manifest": uploadedManifest.manifestId,
9094
- "Status": "live"
9973
+ "Status": "live",
9974
+ "Agent doc": "GRANULAR_SANDBOX.md"
9095
9975
  });
9096
9976
  console.log();
9097
9977
  } catch (err) {
@@ -9404,6 +10284,12 @@ async function devCommand() {
9404
10284
  });
9405
10285
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9406
10286
  spin.succeed(` Build completed in ${totalTime}s \u2014 ${brand.secondary(completed.buildId)}`);
10287
+ writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
10288
+ sandboxId: config.sandboxId,
10289
+ apiUrl: config.apiUrl,
10290
+ manifestId: uploaded.manifestId,
10291
+ buildId: completed.buildId
10292
+ });
9407
10293
  } catch (err) {
9408
10294
  error(`Build failed: ${err.message}`);
9409
10295
  }
@@ -9423,251 +10309,30 @@ async function devCommand() {
9423
10309
  await new Promise(() => {
9424
10310
  });
9425
10311
  }
10312
+
10313
+ // src/cli/commands/document.ts
9426
10314
  async function documentCommand() {
9427
10315
  printHeader();
9428
- const spinner2 = spinner("Generating documentation...");
10316
+ const spinner2 = spinner("Generating GRANULAR_SANDBOX.md...");
9429
10317
  const project = readManifestFile();
9430
10318
  if (!project) {
9431
10319
  spinner2.fail("No granular.json found. Run `granular init` first.");
9432
10320
  return;
9433
10321
  }
9434
10322
  const rc = readRcFile();
9435
- const sandboxId = rc.sandboxId || "my-sandbox";
9436
- const manifest = project.manifest;
9437
- const classes = [];
9438
- const relationships = [];
9439
- for (const vol of manifest.volumes) {
9440
- for (const op of vol.operations) {
9441
- if (op.create && op.extends === "@std/class") {
9442
- classes.push({
9443
- name: op.create,
9444
- fields: op.has || {},
9445
- description: op.description
9446
- // Description might be on the operation in some versions
9447
- });
9448
- }
9449
- if (op.defineRelationship) {
9450
- const rel = op.defineRelationship;
9451
- const type = rel.leftIsMany ? rel.rightIsMany ? "Many-to-Many" : "One-to-Many" : rel.rightIsMany ? "Many-to-One" : "One-to-One";
9452
- relationships.push({
9453
- left: rel.left,
9454
- right: rel.right,
9455
- relName: `${rel.left}.${rel.leftSubmodel} \u2194 ${rel.right}.${rel.rightSubmodel}`,
9456
- type
9457
- });
9458
- }
9459
- }
9460
- }
9461
- const lines = [];
9462
- lines.push(`# ${manifest.name}`);
9463
- if (manifest.description) lines.push(`
9464
- ${manifest.description}`);
9465
- lines.push(`
9466
- > Generated by Granular SDK from \`granular.json\``);
9467
- lines.push(`
9468
- ## Table of Contents`);
9469
- lines.push(`- [Getting Started](#getting-started)`);
9470
- lines.push(`- [Schema Overview](#schema-overview)`);
9471
- lines.push(`- [Integration Guide](#integration-guide)`);
9472
- lines.push(` - [1. Initialize & Connect](#1-initialize--connect)`);
9473
- lines.push(` - [2. Record Objects](#2-record-objects)`);
9474
- lines.push(` - [3. Declare And Register Effects](#3-declare-and-register-effects)`);
9475
- lines.push(` - [4. Submit Jobs](#4-submit-jobs)`);
9476
- lines.push(`
9477
- ## Getting Started`);
9478
- lines.push(`
9479
- 1. **Install the SDK**`);
9480
- lines.push(` \`\`\`bash
9481
- npm install @granular-software/sdk
9482
- \`\`\``);
9483
- lines.push(`
9484
- 2. **Get your API Key**`);
9485
- lines.push(` Get your key from [Granular Dashboard](https://granular.dev) and set it in your environment:`);
9486
- lines.push(` \`\`\`bash
9487
- export GRANULAR_API_KEY=your_key_here
9488
- \`\`\``);
9489
- lines.push(`
9490
- ## Schema Overview`);
9491
- if (classes.length > 0) {
9492
- lines.push(`
9493
- ### Classes`);
9494
- for (const cls of classes) {
9495
- lines.push(`
9496
- #### \`${cls.name}\``);
9497
- if (cls.description) lines.push(`*${cls.description}*`);
9498
- const fieldNames = Object.keys(cls.fields);
9499
- if (fieldNames.length > 0) {
9500
- lines.push(`
9501
- | Field | Type | Description |`);
9502
- lines.push(`|---|---|---|`);
9503
- for (const [key, spec] of Object.entries(cls.fields)) {
9504
- const type = spec.type || "unknown";
9505
- const desc = spec.description || "-";
9506
- lines.push(`| \`${key}\` | \`${type}\` | ${desc} |`);
9507
- }
9508
- } else {
9509
- lines.push(`
9510
- *No fields defined.*`);
9511
- }
9512
- }
9513
- }
9514
- if (relationships.length > 0) {
9515
- lines.push(`
9516
- ### Relationships`);
9517
- lines.push(`
9518
- | Relationship | Type |`);
9519
- lines.push(`|---|---|`);
9520
- for (const rel of relationships) {
9521
- lines.push(`| \`${rel.relName}\` | ${rel.type} |`);
9522
- }
9523
- }
9524
- lines.push(`
9525
- ## Integration Guide`);
9526
- lines.push(`
9527
- ### 1. Initialize & Connect`);
9528
- lines.push(`
9529
- \`\`\`typescript`);
9530
- lines.push(`import { Granular } from '@granular-software/sdk';`);
9531
- lines.push(`
9532
- const granular = new Granular({`);
9533
- lines.push(` apiKey: process.env.GRANULAR_API_KEY!,`);
9534
- lines.push(`});`);
9535
- lines.push(`
9536
- // 1. Connect to the sandbox for one of your app users`);
9537
- lines.push(`const env = await granular.connect({`);
9538
- lines.push(` sandbox: '${sandboxId}',`);
9539
- lines.push(` userId: 'user_123', // Your app's user ID`);
9540
- lines.push(` email: 'user@example.com', // optional`);
9541
- lines.push(` permissions: ['default'], // Permission profile`);
9542
- lines.push(`});`);
9543
- lines.push(`
9544
- console.log('Connected to:', env.environmentId);`);
9545
- lines.push(`\`\`\``);
9546
- lines.push(`
9547
- ### 2. Record Objects`);
9548
- lines.push(`Sync your data into the graph.`);
9549
- if (classes.length > 0) {
9550
- lines.push(`
9551
- \`\`\`typescript`);
9552
- lines.push(`const env = await granular.connect({`);
9553
- lines.push(` sandbox: '${sandboxId}',`);
9554
- lines.push(` userId: 'user_123',`);
9555
- lines.push(` permissions: ['default'],`);
9556
- lines.push(`});`);
9557
- lines.push(``);
9558
- for (const cls of classes) {
9559
- const exampleFields = {};
9560
- for (const [k, v] of Object.entries(cls.fields)) {
9561
- if (v.type === "string") exampleFields[k] = "example_value";
9562
- else if (v.type === "number") exampleFields[k] = 123;
9563
- else if (v.type === "boolean") exampleFields[k] = true;
9564
- else exampleFields[k] = null;
9565
- }
9566
- lines.push(`await env.recordObject({`);
9567
- lines.push(` className: '${cls.name}',`);
9568
- lines.push(` id: 'unique_${cls.name}_id',`);
9569
- lines.push(` label: 'My ${cls.name}',`);
9570
- if (Object.keys(exampleFields).length > 0) {
9571
- if (Object.keys(exampleFields).length > 2) {
9572
- lines.push(` fields: {`);
9573
- for (const [k, v] of Object.entries(exampleFields)) {
9574
- const val = typeof v === "string" ? `'${v}'` : v;
9575
- lines.push(` ${k}: ${val},`);
9576
- }
9577
- lines.push(` },`);
9578
- } else {
9579
- const entries = Object.entries(exampleFields).map(([k, v]) => `${k}: ${typeof v === "string" ? `'${v}'` : v}`).join(", ");
9580
- lines.push(` fields: { ${entries} },`);
9581
- }
9582
- }
9583
- lines.push(`});`);
9584
- lines.push(``);
9585
- }
9586
- lines.push(`\`\`\``);
9587
- } else {
9588
- lines.push(`*(No classes defined in manifest)*`);
9589
- }
9590
- lines.push(`
9591
- ### 3. Declare And Register Effects`);
9592
- lines.push(`Declare effects in your manifest with \`withEffect\`, then register live handlers at sandbox scope. Use effects for actions in external systems, not for graph reads or search.`);
9593
- lines.push(`
9594
- \`\`\`typescript`);
9595
- lines.push(`await granular.registerEffects('your-sandbox-id', [`);
9596
- if (classes.length > 0) {
9597
- const cls = classes[0];
9598
- lines.push(` // Instance method on ${cls.name}`);
9599
- lines.push(` {`);
9600
- lines.push(` name: 'sync_external_action',`);
9601
- lines.push(` description: 'Perform an external action for this ${cls.name}',`);
9602
- lines.push(` className: '${cls.name}',`);
9603
- lines.push(` inputSchema: { type: 'object', properties: { reason: { type: 'string' } } },`);
9604
- lines.push(` outputSchema: { type: 'object', properties: { actionId: { type: 'string' }, status: { type: 'string' } }, required: ['actionId', 'status'] },`);
9605
- lines.push(` handler: async (id, params, ctx) => {`);
9606
- lines.push(` // 'id' is the real-world ID of the ${cls.name}`);
9607
- lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
9608
- lines.push(` return { actionId: \`act_\${id}\`, status: 'queued' };`);
9609
- lines.push(` },`);
9610
- lines.push(` },`);
9611
- }
9612
- lines.push(` // Global effect`);
9613
- lines.push(` {`);
9614
- lines.push(` name: 'notify_team',`);
9615
- lines.push(` description: 'Send a message through an external workflow',`);
9616
- lines.push(` inputSchema: { type: 'object', properties: { msg: { type: 'string' } }, required: ['msg'] },`);
9617
- lines.push(` handler: async (params, ctx) => {`);
9618
- lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
9619
- lines.push(` console.log('Notification:', params.msg);`);
9620
- lines.push(` },`);
9621
- lines.push(` },`);
9622
- lines.push(`]);`);
9623
- lines.push(`\`\`\``);
9624
- lines.push(`
9625
- ### 4. Submit Jobs`);
9626
- lines.push(`Execute code in the sandbox using your domain classes.`);
9627
- lines.push(`
9628
- \`\`\`typescript`);
9629
- lines.push(`const job = await env.submitJob(\``);
9630
- const classImports = classes.map((c) => c.name.charAt(0).toUpperCase() + c.name.slice(1)).join(", ");
9631
- if (classImports) {
9632
- lines.push(` import { ${classImports}, notify_team } from './sandbox-tools';`);
9633
- } else {
9634
- lines.push(` import { notify_team } from './sandbox-tools';`);
10323
+ const sandboxId = rc.sandboxId;
10324
+ if (!sandboxId) {
10325
+ spinner2.fail("No sandbox in .granularrc. Run `granular init` first.");
10326
+ return;
9635
10327
  }
9636
- lines.push(``);
9637
- if (classes.length > 0) {
9638
- const cls = classes[0];
9639
- const ClassName = cls.name.charAt(0).toUpperCase() + cls.name.slice(1);
9640
- lines.push(` // 1. List objects`);
9641
- lines.push(` const items = await ${ClassName}.list({ limit: 10, saveAs: '${cls.name}_items' });`);
9642
- lines.push(` const item = items[0] ?? null;`);
9643
- lines.push(` console.log('Loaded:', item?.id);`);
9644
- lines.push(``);
9645
- lines.push(` // 2. Call instance method`);
9646
- lines.push(` if (item) {`);
9647
- lines.push(` const action = await item.sync_external_action({ reason: 'Example workflow' });`);
9648
- lines.push(` console.log(action);`);
9649
- lines.push(` }`);
9650
- lines.push(``);
9651
- }
9652
- lines.push(` // Call global tool`);
9653
- lines.push(` await notify_team({ msg: 'Job completed' });`);
9654
- lines.push(``);
9655
- lines.push(` return { success: true };`);
9656
- lines.push(`\`);`);
9657
- lines.push(``);
9658
- lines.push(`// Wait for result`);
9659
- lines.push(`const result = await job.result;`);
9660
- lines.push(`console.log(result);`);
9661
- lines.push(`\`\`\``);
9662
- const footer = `
9663
- ---
9664
- *Documentation generated on ${(/* @__PURE__ */ new Date()).toISOString()}*`;
9665
- lines.push(footer);
9666
- const content = lines.join("\n");
9667
- const outFile = path__namespace.join(getProjectRoot(), "GRANULAR.md");
9668
- fs__namespace.writeFileSync(outFile, content, "utf-8");
9669
- spinner2.succeed(`Generated ${brand.bold("GRANULAR.md")}`);
9670
- info(`Open GRANULAR.md to see your project documentation.`);
10328
+ const apiUrl = loadApiUrl();
10329
+ writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
10330
+ sandboxId,
10331
+ apiUrl,
10332
+ buildPending: true
10333
+ });
10334
+ spinner2.succeed(`Generated ${brand.bold("GRANULAR_SANDBOX.md")}`);
10335
+ 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.");
9671
10336
  }
9672
10337
  var SIMULATOR_BASE = "https://app.granular.software/simulator";
9673
10338
  function openUrl(url) {
@@ -9675,13 +10340,19 @@ function openUrl(url) {
9675
10340
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
9676
10341
  child_process.spawn(cmd, [url], { stdio: "ignore", shell: platform === "win32" });
9677
10342
  }
9678
- async function simulateCommand(sandboxIdArg) {
10343
+ async function simulateCommand(sandboxIdArg, options) {
9679
10344
  const sandboxId = sandboxIdArg ?? resolveConfig().sandboxId;
9680
10345
  if (!sandboxId) {
9681
10346
  error("No sandbox ID. Run from a project with `granular init` or pass a sandbox ID: granular simulate <sandbox-id>");
9682
10347
  process.exit(1);
9683
10348
  }
9684
- const url = `${SIMULATOR_BASE}?sandboxId=${encodeURIComponent(sandboxId)}`;
10349
+ const params = new URLSearchParams({
10350
+ sandboxId
10351
+ });
10352
+ if (options?.subjectId) {
10353
+ params.set("subject_id", options.subjectId);
10354
+ }
10355
+ const url = `${SIMULATOR_BASE}?${params.toString()}`;
9685
10356
  info(`Opening simulator: ${url}`);
9686
10357
  openUrl(url);
9687
10358
  }
@@ -9712,11 +10383,13 @@ program2.hook("preAction", () => {
9712
10383
  process.env.GRANULAR_ENDPOINT_MODE = mode;
9713
10384
  }
9714
10385
  });
9715
- program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").option("--template <template>", "Starter ontology template: library|support|delivery").action(async (projectName, opts) => {
10386
+ program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").option("--template <template>", "Starter ontology template: library|support|delivery").option("--agent-docs", "Add AGENTS.md and docs/granular-manifest.md (for AI coding agents)").option("--no-agent-docs", "Skip agent documentation files").action(async (projectName, opts) => {
9716
10387
  try {
9717
10388
  await initCommand(projectName, {
9718
10389
  skipBuild: opts.skipBuild,
9719
- template: opts.template
10390
+ template: opts.template,
10391
+ agentDocs: opts.agentDocs,
10392
+ noAgentDocs: opts.noAgentDocs
9720
10393
  });
9721
10394
  } catch (err) {
9722
10395
  error(err.message);
@@ -9811,7 +10484,7 @@ program2.command("dev").description("Start development mode (watch + auto-rebuil
9811
10484
  process.exit(1);
9812
10485
  }
9813
10486
  });
9814
- program2.command("document").description("Generate GRANULAR.md documentation from your manifest").action(async () => {
10487
+ program2.command("document").description("Generate GRANULAR_SANDBOX.md (agent reference) from granular.json").action(async () => {
9815
10488
  try {
9816
10489
  await documentCommand();
9817
10490
  } catch (err) {
@@ -9819,9 +10492,9 @@ program2.command("document").description("Generate GRANULAR.md documentation fro
9819
10492
  process.exit(1);
9820
10493
  }
9821
10494
  });
9822
- program2.command("simulate [sandbox-id]").description("Open the Granular simulator in the browser for the current (or given) sandbox").action(async (sandboxId) => {
10495
+ program2.command("simulate [sandbox-id]").description("Open the Granular simulator in the browser for the current (or given) sandbox").option("--subject-id <subjectId>", "Open the simulator preloaded for a specific subject").action(async (sandboxId, options) => {
9823
10496
  try {
9824
- await simulateCommand(sandboxId);
10497
+ await simulateCommand(sandboxId, options);
9825
10498
  } catch (err) {
9826
10499
  error(err.message);
9827
10500
  process.exit(1);