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