@granular-software/sdk 0.4.8 → 0.4.10

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
@@ -8660,6 +8660,816 @@ function buildStatus(status) {
8660
8660
  }
8661
8661
  }
8662
8662
 
8663
+ // src/cli/agent-docs/parse-manifest.ts
8664
+ function cardinalityLabel(rel) {
8665
+ const a = rel.leftIsMany ? rel.rightIsMany ? "Many-to-Many" : "One-to-Many" : rel.rightIsMany ? "Many-to-One" : "One-to-One";
8666
+ return a;
8667
+ }
8668
+ function parseManifestContent(manifest) {
8669
+ const classes = [];
8670
+ const relationships = [];
8671
+ const effects = [];
8672
+ for (const vol of manifest.volumes ?? []) {
8673
+ for (const op of vol.operations ?? []) {
8674
+ if (op.create && op.extends === "@std/class") {
8675
+ classes.push({
8676
+ name: op.create,
8677
+ fields: op.has || {},
8678
+ description: op.description
8679
+ });
8680
+ }
8681
+ if (op.defineRelationship) {
8682
+ const rel = op.defineRelationship;
8683
+ relationships.push({
8684
+ left: rel.left,
8685
+ right: rel.right,
8686
+ leftSubmodel: rel.leftSubmodel,
8687
+ rightSubmodel: rel.rightSubmodel,
8688
+ leftIsMany: rel.leftIsMany,
8689
+ rightIsMany: rel.rightIsMany,
8690
+ cardinalityLabel: cardinalityLabel(rel)
8691
+ });
8692
+ }
8693
+ if (op.withEffect) {
8694
+ const w = op.withEffect;
8695
+ effects.push({
8696
+ name: w.name,
8697
+ description: w.description,
8698
+ attachedClass: w.attachedClass,
8699
+ isStatic: w.isStatic,
8700
+ inputSchema: w.inputSchema,
8701
+ outputSchema: w.outputSchema
8702
+ });
8703
+ }
8704
+ }
8705
+ }
8706
+ return { classes, relationships, effects };
8707
+ }
8708
+ function relationshipEdgesForClass(className, rels) {
8709
+ const edges = [];
8710
+ for (const r of rels) {
8711
+ if (r.left === className) {
8712
+ edges.push({
8713
+ submodelKey: r.leftSubmodel,
8714
+ foreignClass: r.right,
8715
+ isMany: r.leftIsMany
8716
+ });
8717
+ }
8718
+ if (r.right === className) {
8719
+ edges.push({
8720
+ submodelKey: r.rightSubmodel,
8721
+ foreignClass: r.left,
8722
+ isMany: r.rightIsMany
8723
+ });
8724
+ }
8725
+ }
8726
+ return edges;
8727
+ }
8728
+ function toPascalCase(name) {
8729
+ return name.charAt(0).toUpperCase() + name.slice(1);
8730
+ }
8731
+
8732
+ // src/cli/agent-docs/concept-blocks.ts
8733
+ function manifestGuideGranularProductSection() {
8734
+ return `## What is Granular?
8735
+
8736
+ **Granular** is a **hosted service** plus **\`@granular-software/sdk\`**. Together they let you:
8737
+
8738
+ 1. **Declare** a **domain** (classes, relationships between entities, and **effects** \u2014 named actions that run in **your** backend).
8739
+ 2. **Build** that declaration from \`granular.json\` so the service compiles types and tooling for a **sandbox** (a workspace identified by \`sbx_\u2026\`).
8740
+ 3. **Store** **records** (instances of your classes) and **run jobs** (code executed in Granular\u2019s **sandbox runtime**) that call generated helpers and effects.
8741
+
8742
+ **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\`.
8743
+
8744
+ ---
8745
+
8746
+ ## Which document to use
8747
+
8748
+ | Need | Open |
8749
+ |------|------|
8750
+ | **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) |
8751
+ | **This repository\u2019s ontology only** (exact class names, relationship keys, effect schemas, snippets for **this** \`sbx_\u2026\`) | [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md) |
8752
+
8753
+ Edit the domain in **git** (\`granular.json\`); the **sandbox** on the service holds the **built** domain and **data**.`;
8754
+ }
8755
+ function manifestGuideMainConceptsSection() {
8756
+ return `## Main concepts
8757
+
8758
+ The rest of this guide assumes these terms.
8759
+
8760
+ | Term | What it means |
8761
+ |------|----------------|
8762
+ | **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. |
8763
+ | **Domain** | The **ontology**: **classes**, **relationships**, and **effects**. Declared in \`granular.json\`, installed on the sandbox by **build**. Same idea as *schema* / *model*. |
8764
+ | **Manifest** | \`granular.json\` (definition under \`manifest\`). **Source** in the repo until you **build**. |
8765
+ | **Build** | \`granular build\` \u2014 uploads and compiles the manifest. Primary **validation**; fixes errors before relying on types. |
8766
+ | **Environment** | Return value of \`granular.connect({ sandbox, \u2026 })\`. Your **server-side** handle for \`recordObject\`, \`submitJob\`, \`graphql\`, etc., bound to one sandbox. |
8767
+ | **Session** | SDK base type; \`Environment\` **extends** \`Session\`. Job/prompt APIs live on \`Session\`; you still use the **\`Environment\`** instance from \`connect()\` in normal apps. |
8768
+ | **Job** | Code string passed to \`environment.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to \`./sandbox-tools\` (generated classes + effect entrypoints). |
8769
+ | **Effect** | Declared with \`withEffect\`; **handler** registered with \`registerEffects\` in **your** process. Jobs invoke effects; handlers do IO outside Granular. |
8770
+ | **Class** | Entity **kind** in the domain (e.g. \`book\`) with \`has\` fields in the manifest. |
8771
+ | **Record / object** | One **instance**: \`className\`, \`id\`, \`fields\`, \`relationships\`. Upsert with \`recordObject\`. |
8772
+ | **Relationship** | Declared link between two classes; defines **property names** and cardinality. Keys appear in \`recordObject({ relationships })\`. |
8773
+
8774
+ ### Data vs actions (short)
8775
+
8776
+ - **In-graph:** classes, fields, instances, relationships \u2014 manipulated with \`recordObject\` and job APIs on generated classes.
8777
+ - **Outside declared data:** **effects** \u2014 declared in the manifest, implemented on your server, called from jobs like functions.
8778
+
8779
+ There is no separate \`granular validate\`: **\`granular build\`** (and its errors) is how you catch manifest mistakes.`;
8780
+ }
8781
+ function manifestGuideEndToEndSection() {
8782
+ return `## From manifest to running app
8783
+
8784
+ | Step | Action | Outcome |
8785
+ |------|--------|---------|
8786
+ | 1 | Edit \`granular.json\` (\`manifest\` \u2192 \`volumes\` \u2192 \`operations\`) | Domain **source** in the repo |
8787
+ | 2 | \`granular build\` | Manifest **uploaded**; domain **compiled** for the configured sandbox; errors surface here |
8788
+ | 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 |
8789
+ | 4 | \`new Granular({ apiKey })\` then \`connect({ sandbox, userId, permissions })\` | **\`Environment\`** for that sandbox |
8790
+ | 5 | \`recordObject\` / \`recordObjects\` | **Records** stored with correct fields and relationship keys |
8791
+ | 6 | \`submitJob(\`\u2026\`)\` with imports from \`./sandbox-tools\` | **Jobs** run; may call **effects** \u2192 your handlers execute and return results |
8792
+
8793
+ **Accuracy tip:** After changing the manifest, always **re-build** before assuming generated types, \`./sandbox-tools\` names, or effect signatures match the file on disk.`;
8794
+ }
8795
+ function sandboxDocMainConceptsSection() {
8796
+ return `## Main concepts (recap)
8797
+
8798
+ | Term | One line |
8799
+ |------|----------|
8800
+ | **Sandbox** | Workspace \`sbx_\u2026\` \u2014 **domain** + **data** for this project. |
8801
+ | **Domain / ontology** | Classes + relationships + effects \u2014 **built** from \`granular.json\`. |
8802
+ | **Manifest** | \`granular.json\` \u2014 **source**; edit here, then \`granular build\`. |
8803
+ | **Environment** | \`connect()\` result \u2014 **record**, **submitJob**, **graphql** for this sandbox. |
8804
+ | **Session** | Base type; \`Environment\` extends it (job APIs). |
8805
+ | **Job** | \`submitJob\` code using \`./sandbox-tools\`. |
8806
+ | **Effect** | Declared in manifest; **handler** in your process. |
8807
+
8808
+ **Full product + manifest how-to:** [docs/granular-manifest.md](docs/granular-manifest.md).`;
8809
+ }
8810
+ function sandboxDocScopeSection(projectName) {
8811
+ return `## How this doc fits with the manifest guide
8812
+
8813
+ | Document | Role |
8814
+ |----------|------|
8815
+ | [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**. |
8816
+ | **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. |
8817
+
8818
+ If **syntax** or **Granular behavior** is unclear, use the manifest guide; use **this** file for **ontology facts** for **${projectName}**.`;
8819
+ }
8820
+
8821
+ // src/cli/agent-docs/manifest-guide.ts
8822
+ function generateManifestAgentGuide(options) {
8823
+ const { projectName } = options;
8824
+ return `# Granular manifest guide (for coding agents)
8825
+
8826
+ 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).
8827
+
8828
+ **Precedence:** User instructions in chat override this file. When in doubt, read \`granular.json\` and [AGENTS.md](../AGENTS.md).
8829
+
8830
+ ---
8831
+
8832
+ ${manifestGuideGranularProductSection()}
8833
+
8834
+ ---
8835
+
8836
+ ${manifestGuideMainConceptsSection()}
8837
+
8838
+ ---
8839
+
8840
+ ${manifestGuideEndToEndSection()}
8841
+
8842
+ ---
8843
+
8844
+ ## \`granular.json\` file shape (read this first)
8845
+
8846
+ The CLI stores the manifest inside a wrapper object. The file must look like:
8847
+
8848
+ \`\`\`json
8849
+ {
8850
+ "manifest": {
8851
+ "schemaVersion": 2,
8852
+ "name": "${projectName}",
8853
+ "description": "\u2026",
8854
+ "volumes": [ \u2026 ]
8855
+ }
8856
+ }
8857
+ \`\`\`
8858
+
8859
+ - **Do not** put \`schemaVersion\` / \`volumes\` at the top level without the \`manifest\` key \u2014 that is invalid for this CLI.
8860
+ - All schema work happens under \`manifest\`.
8861
+
8862
+ Project display name in this file: **${projectName}**.
8863
+
8864
+ ---
8865
+
8866
+ ## Manifest structure (\`schemaVersion: 2\`)
8867
+
8868
+ \`\`\`json
8869
+ {
8870
+ "schemaVersion": 2,
8871
+ "name": "my-app",
8872
+ "description": "optional string",
8873
+ "volumes": [
8874
+ {
8875
+ "name": "schema",
8876
+ "scope": "sandbox",
8877
+ "imports": [
8878
+ { "alias": "@std", "name": "standard_modules", "label": "prod" }
8879
+ ],
8880
+ "operations": [ ]
8881
+ }
8882
+ ]
8883
+ }
8884
+ \`\`\`
8885
+
8886
+ | Field | Meaning |
8887
+ |-------|---------|
8888
+ | \`volumes\` | Logical groups of operations. Starters use one volume named \`schema\`. |
8889
+ | \`scope\` | \`sandbox\` (typical), \`build\`, or \`user\` \u2014 where the volume applies. |
8890
+ | \`imports\` | Brings in \`standard_modules\` under alias \`@std\` so you can \`"extends": "@std/class"\`. |
8891
+ | \`imports[].label\` | Version label (e.g. \`prod\`) for the module. |
8892
+ | \`operations\` | Ordered list of **operations** (see below). |
8893
+
8894
+ ---
8895
+
8896
+ ## Operations: create a class (fields)
8897
+
8898
+ Domain classes **must** extend the standard class prototype:
8899
+
8900
+ \`\`\`json
8901
+ {
8902
+ "create": "invoice",
8903
+ "extends": "@std/class",
8904
+ "has": {
8905
+ "amount_cents": { "type": "number", "description": "Amount in minor units" },
8906
+ "status": { "type": "string", "description": "Workflow status" }
8907
+ }
8908
+ }
8909
+ \`\`\`
8910
+
8911
+ ### Field entries (\`has\`): precise rules
8912
+
8913
+ 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.
8914
+
8915
+ | Property | Required? | Purpose |
8916
+ |----------|-----------|---------|
8917
+ | \`type\` | **Yes** for normal scalar fields | \`string\`, \`number\`, \`boolean\`, \`email\`, \`url\`, \`date\`, etc. |
8918
+ | \`description\` | Strongly recommended | Used in docs and UX. |
8919
+ | \`value\` | Optional | **Default** baked into the model (static metadata), not per-record data. Rare for entity fields. |
8920
+ | \`ref\` | Optional | Advanced reference. Prefer \`defineRelationship\` for links between entities. |
8921
+ | \`has\` | Optional | Nested structure (advanced). |
8922
+
8923
+ **Precision checklist for fields**
8924
+
8925
+ - Use **snake_case** consistently across manifest, \`recordObject\`, and sandbox code.
8926
+ - Every scalar field should have \`"type": "..."\`.
8927
+ - Do **not** store links to other entities as opaque strings when you can use **relationships** (next section).
8928
+
8929
+ ---
8930
+
8931
+ ## Operations: \`defineRelationship\` (model links precisely)
8932
+
8933
+ Relationships connect two classes and fix **property names** and **cardinality** on each side.
8934
+
8935
+ Example (customer \u2194 orders: one customer, many orders; each order has one customer):
8936
+
8937
+ \`\`\`json
8938
+ {
8939
+ "defineRelationship": {
8940
+ "left": "customer",
8941
+ "right": "order",
8942
+ "leftSubmodel": "orders",
8943
+ "rightSubmodel": "customer",
8944
+ "leftIsMany": true,
8945
+ "rightIsMany": false
8946
+ }
8947
+ }
8948
+ \`\`\`
8949
+
8950
+ ### How to read one \`defineRelationship\` block
8951
+
8952
+ | JSON field | Meaning |
8953
+ |------------|---------|
8954
+ | \`left\`, \`right\` | **Class names** (same strings as in \`"create": "customer"\`). |
8955
+ | \`leftSubmodel\` | On an instance of \`left\`, the **property name** pointing toward \`right\` (e.g. \`customer.orders\`). |
8956
+ | \`rightSubmodel\` | On an instance of \`right\`, the **property name** pointing back to \`left\` (e.g. \`order.customer\`). |
8957
+ | \`leftIsMany\` | If \`true\`, when recording a \`left\` instance, this side is a **list** of target ids. |
8958
+ | \`rightIsMany\` | Same for the \`right\` side. |
8959
+
8960
+ ### Mapping to \`recordObject({ relationships })\`
8961
+
8962
+ **Keys** in \`relationships\` are \`leftSubmodel\` or \`rightSubmodel\` depending on which **class** you are recording:
8963
+
8964
+ - Recording a **left** class instance \u2192 use \`leftSubmodel\` as the key (values are \`right\` class ids).
8965
+ - Recording a **right** class instance \u2192 use \`rightSubmodel\` as the key (values are \`left\` class ids).
8966
+
8967
+ **Values** are always **ids of the target objects** (the foreign class), as plain strings:
8968
+
8969
+ - **Many** side \u2192 \`string[]\`.
8970
+ - **One** side \u2192 a single \`string\`.
8971
+
8972
+ Example:
8973
+
8974
+ - \`recordObject({ className: 'customer', id: 'acme', relationships: { orders: ['ord_1', 'ord_2'] } })\`
8975
+ - \`recordObject({ className: 'order', id: 'ord_1', relationships: { customer: 'acme' } })\`
8976
+
8977
+ **Anti-patterns**
8978
+
8979
+ - Storing \`customer_id\` as a raw string field on \`order\` **without** a \`defineRelationship\` \u2014 you lose cardinality and typed navigation in the sandbox.
8980
+ - 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.
8981
+
8982
+ ---
8983
+
8984
+ ## Operations: \`withEffect\` (declare actions outside the sandbox)
8985
+
8986
+ **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).
8987
+
8988
+ \`\`\`json
8989
+ {
8990
+ "withEffect": {
8991
+ "name": "charge_card",
8992
+ "description": "Charge the customer",
8993
+ "attachedClass": "invoice",
8994
+ "isStatic": false,
8995
+ "inputSchema": { "type": "object", "properties": { "amount_cents": { "type": "number" } }, "required": ["amount_cents"] },
8996
+ "outputSchema": { "type": "object", "properties": { "chargeId": { "type": "string" } }, "required": ["chargeId"] },
8997
+ "stability": "stable",
8998
+ "tags": ["billing"]
8999
+ }
9000
+ }
9001
+ \`\`\`
9002
+
9003
+ | Field | Meaning |
9004
+ |-------|---------|
9005
+ | \`name\` | Must match the \`name\` in \`registerEffects\`. |
9006
+ | \`attachedClass\` | Omit for **global** effects. Set for class-bound effects. |
9007
+ | \`isStatic\` | \`true\` \u2192 static method; \`false\` or omit \u2192 **instance** method (handler receives the object **id** first). |
9008
+ | \`inputSchema\` / \`outputSchema\` | JSON Schema; used for codegen and validation. |
9009
+
9010
+ **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.
9011
+
9012
+ **Permissions:** \`connect({ permissions: [...] })\` assigns profiles (e.g. \`default\`) that can allow or deny which effects a user may call.
9013
+
9014
+ ---
9015
+
9016
+ ## IDs and labels
9017
+
9018
+ | Concept | Rule |
9019
+ |---------|------|
9020
+ | \`id\` in \`recordObject\` | **Per-class** unique string you choose. Two different classes *may* reuse the same id string. |
9021
+ | \`label\` | Display label; optional. |
9022
+ | \`recordObject\` | Upserts by \`{ className, id }\`; fields and relationships are reapplied. |
9023
+ | \`relationships\` values | Use the **target object\u2019s id** for its class (see above). |
9024
+
9025
+ ---
9026
+
9027
+ ## \`@granular-software/sdk\` \u2014 capability map (after build)
9028
+
9029
+ ### \`Granular\` client
9030
+
9031
+ | API | Purpose |
9032
+ |-----|---------|
9033
+ | \`new Granular({ apiKey, apiUrl?, endpointMode?, token?, tokenProvider?, \u2026 })\` | Auth. Env: \`GRANULAR_API_KEY\`, \`GRANULAR_API_URL\`, \`GRANULAR_ENDPOINT_MODE\`. |
9034
+ | \`recordUser({ userId, name?, email?, permissions? })\` | Upsert user for later \`connect\`. |
9035
+ | \`connect({ sandbox, userId?, granularId?, user?, permissions?, clientId?, initialHeap? })\` | Opens a session \u2192 **\`Environment\`**. |
9036
+ | \`registerEffects(sandboxId, effects)\` / \`registerEffect\` | Register handlers for manifest \`withEffect\` declarations. |
9037
+ | \`unregisterEffect\` / \`unregisterAllEffects\` / \`disconnectEffects\` | Stop effect handlers for a sandbox. |
9038
+ | \`granular.sandboxes\` | \`.list()\`, \`.get\`, \`.create\`, \`.update\`, \`.delete\` |
9039
+ | \`granular.permissionProfiles\` | Permission profiles per sandbox. |
9040
+ | \`granular.environments\` | List/create/delete environments (usually use \`connect()\`). |
9041
+ | \`granular.subjects\` | Subjects / assignments (see typings). |
9042
+
9043
+ ### \`Environment\` (connected session)
9044
+
9045
+ | API | Purpose |
9046
+ |-----|---------|
9047
+ | \`environmentId\`, \`sandboxId\`, \`apiEndpoint\`, \u2026 | Session context. |
9048
+ | \`applyManifest(manifest)\` | Apply manifest operations at runtime (alternative to CLI build for dynamic ontologies). |
9049
+ | \`recordObject\` / \`recordObjects\` | Upsert instances and relationships. |
9050
+ | \`enqueueRecordImport\`, \`listRecordImports\`, \`getRecordImportSummary\`, \u2026 | Background bulk import. |
9051
+ | \`graphql(query, variables?)\` | **GraphQL** \u2014 see dedicated subsection below. |
9052
+ | \`defineRelationship\`, \`getRelationships\`, \`attach\`, \`detach\`, \`listRelated\` | Imperative relationship operations (same ideas as manifest \`defineRelationship\`). |
9053
+ | \`submitJob(code)\` | Run code in the sandbox; import from \`./sandbox-tools\`. |
9054
+ | \`getDomain()\`, \`getDomainTypes()\`, \`getDomainDocs()\`, \`getDomainDocumentation()\` | Domain summary and generated TypeScript / docs. |
9055
+ | \`getEffects()\` / \`getTools()\`, \`onEffectsChanged()\` | Effect catalog and updates. |
9056
+ | \`checkReadiness()\`, \`on('readiness', \u2026)\` | Environment readiness. |
9057
+ | \`getHeap()\` | Session state snapshot (advanced). |
9058
+ | \`rpc(method, params)\` | Low-level session RPC (advanced). |
9059
+ | \`disconnect()\` | End the session. |
9060
+
9061
+ ### GraphQL API (\`env.graphql\`)
9062
+
9063
+ 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.
9064
+
9065
+ ### \`Session\` (base class)
9066
+
9067
+ | API | Purpose |
9068
+ |-----|---------|
9069
+ | \`submitJob\`, \`answerPrompt\`, job APIs | Jobs and human-in-the-loop prompts. |
9070
+
9071
+ ---
9072
+
9073
+ ## CLI commands
9074
+
9075
+ | Command | Purpose |
9076
+ |---------|---------|
9077
+ | \`granular build\` | Upload \`granular.json\` and compile (primary **validation**). |
9078
+ | \`granular deploy\` | Build + deploy current production. |
9079
+ | \`granular dev\` | Watch manifest, rebuild on change. |
9080
+ | \`granular document\` | Regenerate \`GRANULAR_SANDBOX.md\`. |
9081
+ | \`granular init\` | Scaffold project; optional \`--agent-docs\`. |
9082
+ | \`granular add class|field|relation\` | Mutate local manifest. |
9083
+ | \`granular simulate\` | Open simulator in browser. |
9084
+ | \`granular pull\` | Pull manifest from API. |
9085
+ | \`granular login\`, \`granular whoami\`, \`granular status\` | Auth / project info. |
9086
+
9087
+ ---
9088
+
9089
+ ## After you change the manifest
9090
+
9091
+ 1. \`granular build\` (or \`granular dev\`).
9092
+ 2. Align **effect handlers** with every \`withEffect\` name + schemas.
9093
+ 3. \`granular document\` to refresh \`GRANULAR_SANDBOX.md\` without a full build if needed (build still records manifest/build ids).
9094
+
9095
+ ---
9096
+
9097
+ ## Further reading
9098
+
9099
+ - [docs.granular.dev](https://docs.granular.dev)
9100
+ - Generated sandbox snapshot: [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md)
9101
+ `;
9102
+ }
9103
+
9104
+ // src/cli/agent-docs/agents-md.ts
9105
+ var GRANULAR_AGENTS_BEGIN = "<!-- granular-sdk:begin -->";
9106
+ var GRANULAR_AGENTS_END = "<!-- granular-sdk:end -->";
9107
+ function generateGranularAgentsBlock(options) {
9108
+ const manifestPath = options?.manifestGuidePath ?? "docs/granular-manifest.md";
9109
+ return `${GRANULAR_AGENTS_BEGIN}
9110
+
9111
+ ## Granular (this project)
9112
+
9113
+ **No prior Granular context assumed.** Read in order:
9114
+
9115
+ 1. **[${manifestPath}](${manifestPath})** \u2014 What Granular is, glossary, \`granular.json\` operations, \`granular build\`, \`connect\` / **environment** / **session**, \`submitJob\`, \`recordObject\`, **effects**, SDK + CLI reference.
9116
+ 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.
9117
+
9118
+ **Instruction precedence:** User chat > this section > other content in this file.
9119
+
9120
+ ${GRANULAR_AGENTS_END}`;
9121
+ }
9122
+ function mergeGranularAgentsBlock(existingContent, block) {
9123
+ const trimmed = existingContent.trimEnd();
9124
+ if (trimmed.includes(GRANULAR_AGENTS_BEGIN) && trimmed.includes(GRANULAR_AGENTS_END)) {
9125
+ const re = new RegExp(
9126
+ `${escapeRegex(GRANULAR_AGENTS_BEGIN)}[\\s\\S]*?${escapeRegex(GRANULAR_AGENTS_END)}`,
9127
+ "m"
9128
+ );
9129
+ return existingContent.replace(re, block.trim());
9130
+ }
9131
+ if (!trimmed) {
9132
+ return `${block.trim()}
9133
+ `;
9134
+ }
9135
+ return `${trimmed}
9136
+
9137
+ ${block.trim()}
9138
+ `;
9139
+ }
9140
+ function escapeRegex(s) {
9141
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9142
+ }
9143
+
9144
+ // src/cli/agent-docs/sandbox-doc.ts
9145
+ function exampleScalarValue(type) {
9146
+ if (type === "number") return 0;
9147
+ if (type === "boolean") return true;
9148
+ if (type === "date") return "2026-01-01";
9149
+ if (type === "email") return "user@example.com";
9150
+ if (type === "url") return "https://example.com";
9151
+ return "example";
9152
+ }
9153
+ function formatSchemaSnippet(obj, maxLen = 2e3) {
9154
+ const s = JSON.stringify(obj, null, 2);
9155
+ if (s.length <= maxLen) return s;
9156
+ return `${s.slice(0, maxLen)}
9157
+ /* \u2026 truncated \u2026 */`;
9158
+ }
9159
+ function generateSandboxAgentDoc(manifest, meta) {
9160
+ const parsed = parseManifestContent(manifest);
9161
+ const { classes, relationships, effects } = parsed;
9162
+ const lines = [];
9163
+ const seed = meta.seedScriptName ?? "granular-seed.ts";
9164
+ const eff = meta.effectsScriptName ?? "granular-effects.ts";
9165
+ lines.push(`# ${manifest.name} \u2014 sandbox agent reference`);
9166
+ lines.push("");
9167
+ lines.push(
9168
+ "> **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`."
9169
+ );
9170
+ lines.push("");
9171
+ lines.push("## Sandbox identity");
9172
+ lines.push("");
9173
+ lines.push("| Key | Value |");
9174
+ lines.push("|-----|-------|");
9175
+ lines.push(`| Sandbox id | \`${meta.sandboxId}\` |`);
9176
+ if (meta.apiUrl) lines.push(`| API / WS base | \`${meta.apiUrl}\` |`);
9177
+ if (meta.manifestId) lines.push(`| Last uploaded manifest id | \`${meta.manifestId}\` |`);
9178
+ if (meta.buildId) lines.push(`| Last build id | \`${meta.buildId}\` |`);
9179
+ if (meta.buildPending) {
9180
+ lines.push("| Build status | *No successful build recorded in this doc run \u2014 run `granular build`.* |");
9181
+ }
9182
+ lines.push("");
9183
+ lines.push("Set `GRANULAR_API_KEY` (e.g. in `.env.local`). Optional: `GRANULAR_API_URL` overrides the WebSocket URL.");
9184
+ lines.push("");
9185
+ for (const line of sandboxDocMainConceptsSection().split("\n")) {
9186
+ lines.push(line);
9187
+ }
9188
+ lines.push("");
9189
+ for (const line of sandboxDocScopeSection(manifest.name).split("\n")) {
9190
+ lines.push(line);
9191
+ }
9192
+ lines.push("");
9193
+ lines.push("## Quick CLI");
9194
+ lines.push("");
9195
+ lines.push("| Command | Purpose |");
9196
+ lines.push("|---------|---------|");
9197
+ lines.push("| `granular build` | Upload manifest and build |");
9198
+ lines.push("| `granular document` | Refresh this file from `granular.json` |");
9199
+ lines.push("| `npx tsx " + seed + "` | Seed sample records (if present) |");
9200
+ lines.push("| `npx tsx " + eff + "` | Run effect handler host (if present) |");
9201
+ lines.push("");
9202
+ lines.push("## Connect");
9203
+ lines.push("");
9204
+ lines.push("```typescript");
9205
+ lines.push(`import { Granular } from '@granular-software/sdk';`);
9206
+ lines.push("");
9207
+ lines.push(`const granular = new Granular({`);
9208
+ lines.push(` apiKey: process.env.GRANULAR_API_KEY!,`);
9209
+ lines.push(` // apiUrl: process.env.GRANULAR_API_URL, // optional`);
9210
+ lines.push(`});`);
9211
+ lines.push("");
9212
+ lines.push(`const env = await granular.connect({`);
9213
+ lines.push(` sandbox: '${meta.sandboxId}',`);
9214
+ lines.push(` userId: 'your_app_user_id',`);
9215
+ lines.push(` permissions: ['default'],`);
9216
+ lines.push(`});`);
9217
+ lines.push("```");
9218
+ lines.push("");
9219
+ lines.push("## Classes and fields");
9220
+ lines.push("");
9221
+ if (classes.length === 0) {
9222
+ lines.push("*No `create` + `@std/class` operations found in the manifest.*");
9223
+ lines.push("");
9224
+ } else {
9225
+ for (const cls of classes) {
9226
+ lines.push(`### \`${cls.name}\``);
9227
+ lines.push("");
9228
+ if (cls.description) lines.push(cls.description);
9229
+ lines.push("");
9230
+ const keys = Object.keys(cls.fields);
9231
+ if (keys.length === 0) {
9232
+ lines.push("*No scalar fields in `has`.*");
9233
+ } else {
9234
+ lines.push("| Field | Type | Description |");
9235
+ lines.push("|-------|------|-------------|");
9236
+ for (const k of keys) {
9237
+ const spec = cls.fields[k];
9238
+ const t = spec?.type || "\u2014";
9239
+ const d = spec?.description || "\u2014";
9240
+ lines.push(`| \`${k}\` | \`${String(t)}\` | ${d} |`);
9241
+ }
9242
+ }
9243
+ lines.push("");
9244
+ }
9245
+ }
9246
+ lines.push("## Relationships");
9247
+ lines.push("");
9248
+ 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).");
9249
+ lines.push("");
9250
+ if (relationships.length === 0) {
9251
+ lines.push("*No relationships defined.*");
9252
+ lines.push("");
9253
+ } else {
9254
+ lines.push("| Left | Right | Cardinality | Left key (`leftSubmodel`) | Right key (`rightSubmodel`) |");
9255
+ lines.push("|------|-------|-------------|---------------------------|----------------------------|");
9256
+ for (const r of relationships) {
9257
+ lines.push(
9258
+ `| \`${r.left}\` | \`${r.right}\` | ${r.cardinalityLabel} | \`${r.leftSubmodel}\` | \`${r.rightSubmodel}\` |`
9259
+ );
9260
+ }
9261
+ lines.push("");
9262
+ }
9263
+ lines.push("## `recordObject` \u2014 examples per class");
9264
+ lines.push("");
9265
+ lines.push("`recordObject` upserts by `{ className, id }`. In `relationships`, use **only the target object\u2019s id** for its class (see keys below).");
9266
+ lines.push("");
9267
+ for (const cls of classes) {
9268
+ const edges = relationshipEdgesForClass(cls.name, relationships);
9269
+ lines.push(`### \`${cls.name}\``);
9270
+ lines.push("");
9271
+ lines.push("```typescript");
9272
+ lines.push(`await env.recordObject({`);
9273
+ lines.push(` className: '${cls.name}',`);
9274
+ lines.push(` id: 'your_${cls.name}_id',`);
9275
+ lines.push(` label: 'Label',`);
9276
+ const fieldKeys = Object.keys(cls.fields);
9277
+ if (fieldKeys.length > 0) {
9278
+ lines.push(` fields: {`);
9279
+ for (const k of fieldKeys) {
9280
+ const spec = cls.fields[k];
9281
+ const v = exampleScalarValue(spec?.type);
9282
+ const lit = typeof v === "string" ? `'${v}'` : JSON.stringify(v);
9283
+ lines.push(` ${k}: ${lit},`);
9284
+ }
9285
+ lines.push(` },`);
9286
+ }
9287
+ if (edges.length > 0) {
9288
+ lines.push(` relationships: {`);
9289
+ for (const e of edges) {
9290
+ if (e.isMany) {
9291
+ lines.push(` ${e.submodelKey}: ['${e.foreignClass}_id_1'], // ${e.foreignClass} ids`);
9292
+ } else {
9293
+ lines.push(` ${e.submodelKey}: '${e.foreignClass}_id', // one ${e.foreignClass} id`);
9294
+ }
9295
+ }
9296
+ lines.push(` },`);
9297
+ }
9298
+ lines.push(`});`);
9299
+ lines.push("```");
9300
+ lines.push("");
9301
+ }
9302
+ if (classes.length === 0) {
9303
+ lines.push("*No classes \u2014 no examples.*");
9304
+ lines.push("");
9305
+ }
9306
+ lines.push("## Effects declared in this manifest (`withEffect` + live handlers)");
9307
+ lines.push("");
9308
+ lines.push("Each entry below matches one `withEffect` operation. For every declaration you must:");
9309
+ lines.push("");
9310
+ lines.push("1. **Build** the manifest so the domain package includes the effect (`granular build`).");
9311
+ 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).");
9312
+ 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.");
9313
+ lines.push("");
9314
+ 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`.");
9315
+ lines.push("");
9316
+ if (effects.length === 0) {
9317
+ lines.push("*No `withEffect` operations in this manifest.*");
9318
+ lines.push("");
9319
+ } else {
9320
+ for (const ef of effects) {
9321
+ const scope = ef.attachedClass ? ef.isStatic ? `static on \`${ef.attachedClass}\`` : `instance on \`${ef.attachedClass}\`` : "global";
9322
+ lines.push(`### \`${ef.name}\` (${scope})`);
9323
+ lines.push("");
9324
+ if (ef.description) lines.push(ef.description);
9325
+ lines.push("");
9326
+ lines.push("**Input schema:**");
9327
+ lines.push("");
9328
+ lines.push("```json");
9329
+ lines.push(formatSchemaSnippet(ef.inputSchema));
9330
+ lines.push("```");
9331
+ lines.push("");
9332
+ if (ef.outputSchema) {
9333
+ lines.push("**Output schema:**");
9334
+ lines.push("");
9335
+ lines.push("```json");
9336
+ lines.push(formatSchemaSnippet(ef.outputSchema));
9337
+ lines.push("```");
9338
+ lines.push("");
9339
+ }
9340
+ lines.push("**Handler signature (TypeScript):**");
9341
+ lines.push("");
9342
+ if (!ef.attachedClass) {
9343
+ lines.push("```typescript");
9344
+ lines.push(`// Global: (params, ctx) => Promise<output>`);
9345
+ lines.push(`handler: async (params, ctx) => { /* ... */ }`);
9346
+ lines.push("```");
9347
+ } else if (ef.isStatic) {
9348
+ lines.push("```typescript");
9349
+ lines.push(`// Static: (params, ctx) => Promise<output>`);
9350
+ lines.push(`handler: async (params, ctx) => { /* ... */ }`);
9351
+ lines.push("```");
9352
+ } else {
9353
+ lines.push("```typescript");
9354
+ lines.push(`// Instance: (objectId, params, ctx) => Promise<output>`);
9355
+ lines.push(`handler: async (objectId, params, ctx) => { /* objectId is the ${ef.attachedClass} id */ }`);
9356
+ lines.push("```");
9357
+ }
9358
+ lines.push("");
9359
+ }
9360
+ }
9361
+ lines.push("## `submitJob` / sandbox code");
9362
+ lines.push("");
9363
+ lines.push("Jobs run in the sandbox with generated classes from `./sandbox-tools`. Class names are PascalCase.");
9364
+ lines.push("");
9365
+ const pascalImports = classes.map((c) => toPascalCase(c.name));
9366
+ const effectNames = effects.map((e) => e.name).filter(Boolean);
9367
+ const globalEffects = effects.filter((e) => !e.attachedClass);
9368
+ const firstGlobal = globalEffects[0]?.name;
9369
+ lines.push("```typescript");
9370
+ lines.push(`const job = await env.submitJob(\``);
9371
+ const importList = [...pascalImports, ...effectNames].filter((v, i, a) => a.indexOf(v) === i);
9372
+ if (importList.length > 0) {
9373
+ lines.push(` import { ${importList.join(", ")} } from './sandbox-tools';`);
9374
+ } else {
9375
+ lines.push(` // import generated classes/tools from './sandbox-tools'`);
9376
+ }
9377
+ lines.push("");
9378
+ if (classes.length > 0) {
9379
+ const classWithInstanceEffect = classes.find(
9380
+ (cl) => effects.some((e) => e.attachedClass === cl.name && !e.isStatic)
9381
+ );
9382
+ const target = classWithInstanceEffect ?? classes[0];
9383
+ const C = toPascalCase(target.name);
9384
+ const c = target.name;
9385
+ lines.push(` const rows = await ${C}.list({ limit: 10, saveAs: '${c}_rows' });`);
9386
+ lines.push(` const row = rows[0] ?? null;`);
9387
+ lines.push("");
9388
+ const instEffects = effects.filter((e) => e.attachedClass === c && !e.isStatic);
9389
+ if (instEffects.length > 0) {
9390
+ const n = instEffects[0].name;
9391
+ lines.push(` if (row) {`);
9392
+ lines.push(` const out = await row.${n}({ /* input per schema */ });`);
9393
+ lines.push(` console.log(out);`);
9394
+ lines.push(` }`);
9395
+ } else {
9396
+ lines.push(` console.log(row?.id);`);
9397
+ }
9398
+ lines.push("");
9399
+ }
9400
+ if (firstGlobal) {
9401
+ lines.push(` await ${firstGlobal}({ /* input per schema */ });`);
9402
+ lines.push("");
9403
+ }
9404
+ lines.push(` return { ok: true };`);
9405
+ lines.push(`\`);`);
9406
+ lines.push("");
9407
+ lines.push(`const result = await job.result;`);
9408
+ lines.push("```");
9409
+ lines.push("");
9410
+ lines.push("## Companion scripts");
9411
+ lines.push("");
9412
+ lines.push(`- **\`${seed}\`** \u2014 example \`recordObject\` batch for starter data.`);
9413
+ lines.push(`- **\`${eff}\`** \u2014 long-running process that registers effect handlers for this sandbox.`);
9414
+ lines.push("");
9415
+ lines.push("## SDK surface (reminder)");
9416
+ lines.push("");
9417
+ lines.push("| Area | APIs |");
9418
+ lines.push("|------|------|");
9419
+ lines.push("| Auth / session | `Granular`, `connect`, `disconnect`, `recordUser` |");
9420
+ lines.push("| Data | `recordObject`, `recordObjects`, record import queue APIs |");
9421
+ lines.push("| GraphQL API | `graphql` \u2014 query/mutate the underlying graph when you need it |");
9422
+ lines.push("| Relationships (API) | `defineRelationship`, `getRelationships`, `attach`, `detach`, `listRelated` |");
9423
+ lines.push("| Ontology | `applyManifest` (runtime), or CLI `granular build` |");
9424
+ lines.push("| Jobs | `submitJob`, `answerPrompt` |");
9425
+ lines.push("| Domain | `getDomain`, `getDomainTypes`, `getDomainDocumentation` |");
9426
+ lines.push("| Effects | `registerEffects`, `getEffects`, `onEffectsChanged` |");
9427
+ lines.push("| Ops | `checkReadiness`, `getHeap`, `rpc` |");
9428
+ lines.push("");
9429
+ lines.push("Full list: [docs/granular-manifest.md](docs/granular-manifest.md).");
9430
+ lines.push("");
9431
+ lines.push("---");
9432
+ lines.push("");
9433
+ lines.push(`*Generated at ${(/* @__PURE__ */ new Date()).toISOString()} by @granular-software/sdk*`);
9434
+ lines.push("");
9435
+ return lines.join("\n");
9436
+ }
9437
+ var MANIFEST_GUIDE_RELATIVE = path__namespace.join("docs", "granular-manifest.md");
9438
+ var SANDBOX_DOC_FILENAME = "GRANULAR_SANDBOX.md";
9439
+ function getAgentsMdPath(projectRoot) {
9440
+ return path__namespace.join(projectRoot, "AGENTS.md");
9441
+ }
9442
+ function getManifestGuidePath(projectRoot) {
9443
+ return path__namespace.join(projectRoot, MANIFEST_GUIDE_RELATIVE);
9444
+ }
9445
+ function getSandboxDocPath(projectRoot) {
9446
+ return path__namespace.join(projectRoot, SANDBOX_DOC_FILENAME);
9447
+ }
9448
+ function writeManifestAgentDocs(projectRoot, projectName) {
9449
+ const docsDir = path__namespace.join(projectRoot, "docs");
9450
+ if (!fs__namespace.existsSync(docsDir)) {
9451
+ fs__namespace.mkdirSync(docsDir, { recursive: true });
9452
+ }
9453
+ const guidePath = getManifestGuidePath(projectRoot);
9454
+ const guide = generateManifestAgentGuide({ projectName });
9455
+ fs__namespace.writeFileSync(guidePath, guide, "utf-8");
9456
+ const agentsPath = getAgentsMdPath(projectRoot);
9457
+ const block = generateGranularAgentsBlock({ manifestGuidePath: MANIFEST_GUIDE_RELATIVE.replace(/\\/g, "/") });
9458
+ const previous = fs__namespace.existsSync(agentsPath) ? fs__namespace.readFileSync(agentsPath, "utf-8") : "";
9459
+ const merged = mergeGranularAgentsBlock(previous, block);
9460
+ fs__namespace.writeFileSync(agentsPath, merged, "utf-8");
9461
+ }
9462
+ function writeSandboxAgentDocFile(projectRoot, manifest, meta) {
9463
+ const metaWithScripts = {
9464
+ ...meta,
9465
+ seedScriptName: meta.seedScriptName ?? SEED_SCRIPT_NAME,
9466
+ effectsScriptName: meta.effectsScriptName ?? EFFECTS_SCRIPT_NAME
9467
+ };
9468
+ const body = generateSandboxAgentDoc(manifest, metaWithScripts);
9469
+ const out = getSandboxDocPath(projectRoot);
9470
+ fs__namespace.writeFileSync(out, body, "utf-8");
9471
+ }
9472
+
8663
9473
  // src/cli/commands/init.ts
8664
9474
  function prompt(question, defaultValue) {
8665
9475
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
@@ -8726,6 +9536,10 @@ async function selectStarterTemplate(requestedTemplate) {
8726
9536
  }
8727
9537
  async function initCommand(projectName, options) {
8728
9538
  printHeader();
9539
+ if (options?.agentDocs && options?.noAgentDocs) {
9540
+ error("Cannot use --agent-docs and --no-agent-docs together.");
9541
+ process.exit(1);
9542
+ }
8729
9543
  if (manifestExists()) {
8730
9544
  warn("A granular.json already exists in this directory.");
8731
9545
  const overwrite = await confirm("Overwrite?", false);
@@ -8803,6 +9617,25 @@ async function initCommand(projectName, options) {
8803
9617
  success(`Created ${brand.bold(".granularrc")}`);
8804
9618
  ensureGitignore();
8805
9619
  success("Updated .gitignore");
9620
+ let wantAgentDocs = false;
9621
+ if (options?.agentDocs) {
9622
+ wantAgentDocs = true;
9623
+ } else if (options?.noAgentDocs) {
9624
+ wantAgentDocs = false;
9625
+ } else if (process.stdin.isTTY && process.stdout.isTTY) {
9626
+ console.log();
9627
+ wantAgentDocs = await confirm(
9628
+ "Add agent documentation (AGENTS.md + docs/granular-manifest.md for AI coding tools)?",
9629
+ true
9630
+ );
9631
+ }
9632
+ if (wantAgentDocs) {
9633
+ writeManifestAgentDocs(getProjectRoot(), name);
9634
+ success(`Created ${brand.bold("AGENTS.md")} and ${brand.bold("docs/granular-manifest.md")}`);
9635
+ }
9636
+ let lastManifestId;
9637
+ let lastBuildId;
9638
+ let initialBuildSucceeded = false;
8806
9639
  console.log();
8807
9640
  const shouldBuild = options?.skipBuild ? false : await confirm("Trigger initial build?", true);
8808
9641
  if (shouldBuild) {
@@ -8814,12 +9647,15 @@ async function initCommand(projectName, options) {
8814
9647
  project.manifest,
8815
9648
  "1.0.0"
8816
9649
  );
9650
+ lastManifestId = manifest.manifestId;
8817
9651
  building.text = " Triggering build...";
8818
9652
  const build = await api.triggerBuild(sandbox.sandboxId, manifest.manifestId);
8819
9653
  building.text = " Building...";
8820
9654
  const completed = await api.waitForBuild(build.buildId, (status) => {
8821
9655
  building.text = ` Building... ${brand.muted(status)}`;
8822
9656
  });
9657
+ lastBuildId = completed.buildId;
9658
+ initialBuildSucceeded = true;
8823
9659
  building.succeed(` Build completed: ${brand.secondary(completed.buildId)}`);
8824
9660
  } catch (err) {
8825
9661
  building.fail(` Build failed: ${err.message}`);
@@ -8840,20 +9676,34 @@ async function initCommand(projectName, options) {
8840
9676
  "utf-8"
8841
9677
  );
8842
9678
  success(`Created ${brand.bold(SEED_SCRIPT_NAME)} (sample record seeding script)`);
9679
+ writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
9680
+ sandboxId: sandbox.sandboxId,
9681
+ apiUrl,
9682
+ manifestId: lastManifestId,
9683
+ buildId: lastBuildId,
9684
+ buildPending: !initialBuildSucceeded
9685
+ });
9686
+ success(`Wrote ${brand.bold("GRANULAR_SANDBOX.md")} (agent reference for this sandbox)`);
8843
9687
  console.log();
8844
9688
  divider();
8845
9689
  console.log();
8846
9690
  success(`Project ${brand.bold(name)} initialized!`);
8847
9691
  console.log();
8848
- keyValue({
9692
+ const kv = {
8849
9693
  "Sandbox": sandbox.sandboxId,
8850
9694
  "Template": template.label,
8851
9695
  "Manifest": "granular.json",
8852
9696
  "Config": ".granularrc",
8853
9697
  "API Key": ".env.local",
8854
9698
  "Seed script": SEED_SCRIPT_NAME,
8855
- "Effects script": EFFECTS_SCRIPT_NAME
8856
- });
9699
+ "Effects script": EFFECTS_SCRIPT_NAME,
9700
+ "Sandbox agent doc": "GRANULAR_SANDBOX.md"
9701
+ };
9702
+ if (wantAgentDocs) {
9703
+ kv["Agent manifest guide"] = "docs/granular-manifest.md";
9704
+ kv["Agent index"] = "AGENTS.md";
9705
+ }
9706
+ keyValue(kv);
8857
9707
  const nextSteps2 = options?.skipBuild ? [
8858
9708
  { command: "granular build", description: "Build the starter ontology before you seed or simulate" },
8859
9709
  { command: `npx tsx ${SEED_SCRIPT_NAME}`, description: "Push the sample records into your sandbox" },
@@ -9040,12 +9890,19 @@ async function buildCommand() {
9040
9890
  });
9041
9891
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9042
9892
  building.succeed(` Build completed in ${totalTime}s`);
9893
+ writeSandboxAgentDocFile(getProjectRoot(), manifest, {
9894
+ sandboxId: config.sandboxId,
9895
+ apiUrl: config.apiUrl,
9896
+ manifestId: uploadedManifest.manifestId,
9897
+ buildId: completed.buildId
9898
+ });
9043
9899
  console.log();
9044
9900
  keyValue({
9045
9901
  "Build ID": completed.buildId,
9046
9902
  "Manifest": uploadedManifest.manifestId,
9047
9903
  "Status": "completed",
9048
- "Duration": `${totalTime}s`
9904
+ "Duration": `${totalTime}s`,
9905
+ "Agent doc": "GRANULAR_SANDBOX.md"
9049
9906
  });
9050
9907
  console.log();
9051
9908
  } catch (err) {
@@ -9063,9 +9920,9 @@ async function deployCommand() {
9063
9920
  process.exit(1);
9064
9921
  }
9065
9922
  const api = new ApiClient(config.apiKey, config.apiUrl);
9066
- const manifest = config.project.manifest;
9067
9923
  step("Deploy", `Sandbox ${config.sandboxId}`);
9068
9924
  console.log();
9925
+ const manifest = config.project.manifest;
9069
9926
  const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
9070
9927
  let uploadedManifest;
9071
9928
  try {
@@ -9085,13 +9942,20 @@ async function deployCommand() {
9085
9942
  });
9086
9943
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9087
9944
  building.succeed(` Build completed in ${totalTime}s`);
9945
+ writeSandboxAgentDocFile(getProjectRoot(), manifest, {
9946
+ sandboxId: config.sandboxId,
9947
+ apiUrl: config.apiUrl,
9948
+ manifestId: uploadedManifest.manifestId,
9949
+ buildId: completed.buildId
9950
+ });
9088
9951
  console.log();
9089
9952
  success(`Deployed ${brand.bold(manifest.name)} successfully!`);
9090
9953
  console.log();
9091
9954
  keyValue({
9092
9955
  "Build": completed.buildId,
9093
9956
  "Manifest": uploadedManifest.manifestId,
9094
- "Status": "live"
9957
+ "Status": "live",
9958
+ "Agent doc": "GRANULAR_SANDBOX.md"
9095
9959
  });
9096
9960
  console.log();
9097
9961
  } catch (err) {
@@ -9404,6 +10268,12 @@ async function devCommand() {
9404
10268
  });
9405
10269
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9406
10270
  spin.succeed(` Build completed in ${totalTime}s \u2014 ${brand.secondary(completed.buildId)}`);
10271
+ writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
10272
+ sandboxId: config.sandboxId,
10273
+ apiUrl: config.apiUrl,
10274
+ manifestId: uploaded.manifestId,
10275
+ buildId: completed.buildId
10276
+ });
9407
10277
  } catch (err) {
9408
10278
  error(`Build failed: ${err.message}`);
9409
10279
  }
@@ -9423,251 +10293,30 @@ async function devCommand() {
9423
10293
  await new Promise(() => {
9424
10294
  });
9425
10295
  }
10296
+
10297
+ // src/cli/commands/document.ts
9426
10298
  async function documentCommand() {
9427
10299
  printHeader();
9428
- const spinner2 = spinner("Generating documentation...");
10300
+ const spinner2 = spinner("Generating GRANULAR_SANDBOX.md...");
9429
10301
  const project = readManifestFile();
9430
10302
  if (!project) {
9431
10303
  spinner2.fail("No granular.json found. Run `granular init` first.");
9432
10304
  return;
9433
10305
  }
9434
10306
  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';`);
10307
+ const sandboxId = rc.sandboxId;
10308
+ if (!sandboxId) {
10309
+ spinner2.fail("No sandbox in .granularrc. Run `granular init` first.");
10310
+ return;
9635
10311
  }
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.`);
10312
+ const apiUrl = loadApiUrl();
10313
+ writeSandboxAgentDocFile(getProjectRoot(), project.manifest, {
10314
+ sandboxId,
10315
+ apiUrl,
10316
+ buildPending: true
10317
+ });
10318
+ spinner2.succeed(`Generated ${brand.bold("GRANULAR_SANDBOX.md")}`);
10319
+ 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
10320
  }
9672
10321
  var SIMULATOR_BASE = "https://app.granular.software/simulator";
9673
10322
  function openUrl(url) {
@@ -9712,11 +10361,13 @@ program2.hook("preAction", () => {
9712
10361
  process.env.GRANULAR_ENDPOINT_MODE = mode;
9713
10362
  }
9714
10363
  });
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) => {
10364
+ 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
10365
  try {
9717
10366
  await initCommand(projectName, {
9718
10367
  skipBuild: opts.skipBuild,
9719
- template: opts.template
10368
+ template: opts.template,
10369
+ agentDocs: opts.agentDocs,
10370
+ noAgentDocs: opts.noAgentDocs
9720
10371
  });
9721
10372
  } catch (err) {
9722
10373
  error(err.message);
@@ -9811,7 +10462,7 @@ program2.command("dev").description("Start development mode (watch + auto-rebuil
9811
10462
  process.exit(1);
9812
10463
  }
9813
10464
  });
9814
- program2.command("document").description("Generate GRANULAR.md documentation from your manifest").action(async () => {
10465
+ program2.command("document").description("Generate GRANULAR_SANDBOX.md (agent reference) from granular.json").action(async () => {
9815
10466
  try {
9816
10467
  await documentCommand();
9817
10468
  } catch (err) {