@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/README.md +8 -0
- package/dist/cli/index.js +896 -245
- package/dist/index.d.mts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +303 -111
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -111
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
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
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
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
|
|
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) {
|