@granular-software/sdk 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1269 -371
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5434,6 +5434,1182 @@ function resolveAuthUrl(explicitAuthUrl, mode) {
|
|
|
5434
5434
|
return resolveEndpointMode(mode) === "local" ? LOCAL_AUTH_URL : PRODUCTION_AUTH_URL;
|
|
5435
5435
|
}
|
|
5436
5436
|
|
|
5437
|
+
// src/cli/starter-templates.ts
|
|
5438
|
+
var DEFAULT_STARTER_TEMPLATE_ID = "library";
|
|
5439
|
+
var EFFECTS_SCRIPT_NAME = "granular-effects.ts";
|
|
5440
|
+
var SEED_SCRIPT_NAME = "granular-seed.ts";
|
|
5441
|
+
function createClassOperation(name, fields) {
|
|
5442
|
+
return {
|
|
5443
|
+
create: name,
|
|
5444
|
+
extends: "@std/class",
|
|
5445
|
+
has: fields
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
function createRelationshipOperation(definition) {
|
|
5449
|
+
return {
|
|
5450
|
+
defineRelationship: definition
|
|
5451
|
+
};
|
|
5452
|
+
}
|
|
5453
|
+
function createEffectOperation(effect) {
|
|
5454
|
+
return {
|
|
5455
|
+
withEffect: {
|
|
5456
|
+
name: effect.name,
|
|
5457
|
+
description: effect.description,
|
|
5458
|
+
attachedClass: effect.className,
|
|
5459
|
+
isStatic: effect.static,
|
|
5460
|
+
inputSchema: effect.inputSchema,
|
|
5461
|
+
outputSchema: effect.outputSchema
|
|
5462
|
+
}
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
var LIBRARY_TEMPLATE = {
|
|
5466
|
+
id: "library",
|
|
5467
|
+
label: "Library Catalog",
|
|
5468
|
+
summary: "A connected book catalog with clear graph queries and real-world reader actions.",
|
|
5469
|
+
manifestDescription: "A connected book catalog with authors, books, publishers, editors, and external reader actions.",
|
|
5470
|
+
operations: [
|
|
5471
|
+
createClassOperation("author", {
|
|
5472
|
+
name: { type: "string", description: "Author full name" },
|
|
5473
|
+
nationality: { type: "string", description: "Primary nationality or home region" },
|
|
5474
|
+
birth_year: { type: "number", description: "Year of birth" }
|
|
5475
|
+
}),
|
|
5476
|
+
createClassOperation("book", {
|
|
5477
|
+
title: { type: "string", description: "Book title" },
|
|
5478
|
+
isbn: { type: "string", description: "ISBN identifier" },
|
|
5479
|
+
publication_year: { type: "number", description: "Year the book was first published" },
|
|
5480
|
+
price_eur: { type: "number", description: "Current list price in euros" }
|
|
5481
|
+
}),
|
|
5482
|
+
createClassOperation("publisher", {
|
|
5483
|
+
name: { type: "string", description: "Publisher name" },
|
|
5484
|
+
country: { type: "string", description: "Main publishing market" },
|
|
5485
|
+
website: { type: "url", description: "Public website" }
|
|
5486
|
+
}),
|
|
5487
|
+
createClassOperation("editor", {
|
|
5488
|
+
name: { type: "string", description: "Editor full name" },
|
|
5489
|
+
specialty: { type: "string", description: "Editorial focus or genre specialty" },
|
|
5490
|
+
email: { type: "email", description: "Work email address" }
|
|
5491
|
+
}),
|
|
5492
|
+
createRelationshipOperation({
|
|
5493
|
+
left: "author",
|
|
5494
|
+
right: "book",
|
|
5495
|
+
leftSubmodel: "books",
|
|
5496
|
+
rightSubmodel: "authors",
|
|
5497
|
+
leftIsMany: true,
|
|
5498
|
+
rightIsMany: true
|
|
5499
|
+
}),
|
|
5500
|
+
createRelationshipOperation({
|
|
5501
|
+
left: "publisher",
|
|
5502
|
+
right: "book",
|
|
5503
|
+
leftSubmodel: "books",
|
|
5504
|
+
rightSubmodel: "publisher",
|
|
5505
|
+
leftIsMany: true,
|
|
5506
|
+
rightIsMany: false
|
|
5507
|
+
}),
|
|
5508
|
+
createRelationshipOperation({
|
|
5509
|
+
left: "editor",
|
|
5510
|
+
right: "book",
|
|
5511
|
+
leftSubmodel: "books",
|
|
5512
|
+
rightSubmodel: "editor",
|
|
5513
|
+
leftIsMany: true,
|
|
5514
|
+
rightIsMany: false
|
|
5515
|
+
}),
|
|
5516
|
+
createRelationshipOperation({
|
|
5517
|
+
left: "publisher",
|
|
5518
|
+
right: "editor",
|
|
5519
|
+
leftSubmodel: "editors",
|
|
5520
|
+
rightSubmodel: "publisher",
|
|
5521
|
+
leftIsMany: true,
|
|
5522
|
+
rightIsMany: false
|
|
5523
|
+
})
|
|
5524
|
+
],
|
|
5525
|
+
seedUser: {
|
|
5526
|
+
userId: "reader_demo",
|
|
5527
|
+
name: "Mina Reader",
|
|
5528
|
+
email: "mina@example.com",
|
|
5529
|
+
permissions: ["default"]
|
|
5530
|
+
},
|
|
5531
|
+
seedRecords: [
|
|
5532
|
+
{
|
|
5533
|
+
className: "publisher",
|
|
5534
|
+
id: "rivergate_press",
|
|
5535
|
+
label: "Rivergate Press",
|
|
5536
|
+
fields: {
|
|
5537
|
+
name: "Rivergate Press",
|
|
5538
|
+
country: "France",
|
|
5539
|
+
website: "https://rivergate.example"
|
|
5540
|
+
}
|
|
5541
|
+
},
|
|
5542
|
+
{
|
|
5543
|
+
className: "publisher",
|
|
5544
|
+
id: "northwind_books",
|
|
5545
|
+
label: "Northwind Books",
|
|
5546
|
+
fields: {
|
|
5547
|
+
name: "Northwind Books",
|
|
5548
|
+
country: "United Kingdom",
|
|
5549
|
+
website: "https://northwind.example"
|
|
5550
|
+
}
|
|
5551
|
+
},
|
|
5552
|
+
{
|
|
5553
|
+
className: "editor",
|
|
5554
|
+
id: "maya_chen",
|
|
5555
|
+
label: "Maya Chen",
|
|
5556
|
+
fields: {
|
|
5557
|
+
name: "Maya Chen",
|
|
5558
|
+
specialty: "Speculative fiction",
|
|
5559
|
+
email: "maya.chen@rivergate.example"
|
|
5560
|
+
},
|
|
5561
|
+
relationships: {
|
|
5562
|
+
publisher: "rivergate_press"
|
|
5563
|
+
}
|
|
5564
|
+
},
|
|
5565
|
+
{
|
|
5566
|
+
className: "editor",
|
|
5567
|
+
id: "olivier_marchand",
|
|
5568
|
+
label: "Olivier Marchand",
|
|
5569
|
+
fields: {
|
|
5570
|
+
name: "Olivier Marchand",
|
|
5571
|
+
specialty: "Literary fiction",
|
|
5572
|
+
email: "olivier@northwind.example"
|
|
5573
|
+
},
|
|
5574
|
+
relationships: {
|
|
5575
|
+
publisher: "northwind_books"
|
|
5576
|
+
}
|
|
5577
|
+
},
|
|
5578
|
+
{
|
|
5579
|
+
className: "author",
|
|
5580
|
+
id: "ursula_le_guin",
|
|
5581
|
+
label: "Ursula K. Le Guin",
|
|
5582
|
+
fields: {
|
|
5583
|
+
name: "Ursula K. Le Guin",
|
|
5584
|
+
nationality: "United States",
|
|
5585
|
+
birth_year: 1929
|
|
5586
|
+
}
|
|
5587
|
+
},
|
|
5588
|
+
{
|
|
5589
|
+
className: "author",
|
|
5590
|
+
id: "jrr_tolkien",
|
|
5591
|
+
label: "J.R.R. Tolkien",
|
|
5592
|
+
fields: {
|
|
5593
|
+
name: "J.R.R. Tolkien",
|
|
5594
|
+
nationality: "United Kingdom",
|
|
5595
|
+
birth_year: 1892
|
|
5596
|
+
}
|
|
5597
|
+
},
|
|
5598
|
+
{
|
|
5599
|
+
className: "author",
|
|
5600
|
+
id: "madeline_miller",
|
|
5601
|
+
label: "Madeline Miller",
|
|
5602
|
+
fields: {
|
|
5603
|
+
name: "Madeline Miller",
|
|
5604
|
+
nationality: "United States",
|
|
5605
|
+
birth_year: 1978
|
|
5606
|
+
}
|
|
5607
|
+
},
|
|
5608
|
+
{
|
|
5609
|
+
className: "book",
|
|
5610
|
+
id: "left_hand_of_darkness",
|
|
5611
|
+
label: "The Left Hand of Darkness",
|
|
5612
|
+
fields: {
|
|
5613
|
+
title: "The Left Hand of Darkness",
|
|
5614
|
+
isbn: "978-0-441-47812-5",
|
|
5615
|
+
publication_year: 1969,
|
|
5616
|
+
price_eur: 14
|
|
5617
|
+
},
|
|
5618
|
+
relationships: {
|
|
5619
|
+
authors: ["ursula_le_guin"],
|
|
5620
|
+
publisher: "rivergate_press",
|
|
5621
|
+
editor: "maya_chen"
|
|
5622
|
+
}
|
|
5623
|
+
},
|
|
5624
|
+
{
|
|
5625
|
+
className: "book",
|
|
5626
|
+
id: "wizard_of_earthsea",
|
|
5627
|
+
label: "A Wizard of Earthsea",
|
|
5628
|
+
fields: {
|
|
5629
|
+
title: "A Wizard of Earthsea",
|
|
5630
|
+
isbn: "978-0-547-77274-1",
|
|
5631
|
+
publication_year: 1968,
|
|
5632
|
+
price_eur: 12
|
|
5633
|
+
},
|
|
5634
|
+
relationships: {
|
|
5635
|
+
authors: ["ursula_le_guin"],
|
|
5636
|
+
publisher: "rivergate_press",
|
|
5637
|
+
editor: "maya_chen"
|
|
5638
|
+
}
|
|
5639
|
+
},
|
|
5640
|
+
{
|
|
5641
|
+
className: "book",
|
|
5642
|
+
id: "lord_of_the_rings",
|
|
5643
|
+
label: "The Lord of the Rings",
|
|
5644
|
+
fields: {
|
|
5645
|
+
title: "The Lord of the Rings",
|
|
5646
|
+
isbn: "978-0-618-64015-7",
|
|
5647
|
+
publication_year: 1954,
|
|
5648
|
+
price_eur: 19
|
|
5649
|
+
},
|
|
5650
|
+
relationships: {
|
|
5651
|
+
authors: ["jrr_tolkien"],
|
|
5652
|
+
publisher: "northwind_books",
|
|
5653
|
+
editor: "olivier_marchand"
|
|
5654
|
+
}
|
|
5655
|
+
},
|
|
5656
|
+
{
|
|
5657
|
+
className: "book",
|
|
5658
|
+
id: "song_of_achilles",
|
|
5659
|
+
label: "The Song of Achilles",
|
|
5660
|
+
fields: {
|
|
5661
|
+
title: "The Song of Achilles",
|
|
5662
|
+
isbn: "978-0-06-206062-4",
|
|
5663
|
+
publication_year: 2011,
|
|
5664
|
+
price_eur: 16
|
|
5665
|
+
},
|
|
5666
|
+
relationships: {
|
|
5667
|
+
authors: ["madeline_miller"],
|
|
5668
|
+
publisher: "northwind_books",
|
|
5669
|
+
editor: "olivier_marchand"
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
],
|
|
5673
|
+
runtimeSetup: [
|
|
5674
|
+
"const bookmarkLog: Array<{ bookmarkId: string; bookId: string; userId: string; collection: string; note?: string }> = [];",
|
|
5675
|
+
"const orderLog: Array<{ orderId: string; bookId: string; format: string; quantity: number; userId: string }> = [];",
|
|
5676
|
+
"const followLog: Array<{ subscriptionId: string; authorId: string; channel: string; userId: string }> = [];",
|
|
5677
|
+
"const deliveryLog: Array<{ deliveryId: string; email: string; bookIds: string[]; requestedBy: string }> = [];"
|
|
5678
|
+
],
|
|
5679
|
+
effects: [
|
|
5680
|
+
{
|
|
5681
|
+
name: "bookmark",
|
|
5682
|
+
description: "Save this book to the current reader bookmark service",
|
|
5683
|
+
className: "book",
|
|
5684
|
+
inputSchema: {
|
|
5685
|
+
type: "object",
|
|
5686
|
+
properties: {
|
|
5687
|
+
collection: { type: "string", description: "Destination collection name" },
|
|
5688
|
+
note: { type: "string", description: "Optional note attached to the bookmark" }
|
|
5689
|
+
}
|
|
5690
|
+
},
|
|
5691
|
+
outputSchema: {
|
|
5692
|
+
type: "object",
|
|
5693
|
+
properties: {
|
|
5694
|
+
bookmarkId: { type: "string" },
|
|
5695
|
+
status: { type: "string" },
|
|
5696
|
+
savedIn: { type: "string" }
|
|
5697
|
+
},
|
|
5698
|
+
required: ["bookmarkId", "status", "savedIn"]
|
|
5699
|
+
},
|
|
5700
|
+
handlerBody: `const bookmarkId = createId('bookmark');
|
|
5701
|
+
const collection = input.collection ?? 'saved';
|
|
5702
|
+
bookmarkLog.push({
|
|
5703
|
+
bookmarkId,
|
|
5704
|
+
bookId: objectId,
|
|
5705
|
+
userId: ctx.user.userId,
|
|
5706
|
+
collection,
|
|
5707
|
+
note: input.note,
|
|
5708
|
+
});
|
|
5709
|
+
return {
|
|
5710
|
+
bookmarkId,
|
|
5711
|
+
status: 'saved',
|
|
5712
|
+
savedIn: collection,
|
|
5713
|
+
};`
|
|
5714
|
+
},
|
|
5715
|
+
{
|
|
5716
|
+
name: "buy",
|
|
5717
|
+
description: "Create a checkout order for this book in the commerce system",
|
|
5718
|
+
className: "book",
|
|
5719
|
+
inputSchema: {
|
|
5720
|
+
type: "object",
|
|
5721
|
+
properties: {
|
|
5722
|
+
format: { type: "string", description: "Requested format such as paperback, ebook, or audio" },
|
|
5723
|
+
quantity: { type: "number", description: "How many copies to order" }
|
|
5724
|
+
},
|
|
5725
|
+
required: ["format"]
|
|
5726
|
+
},
|
|
5727
|
+
outputSchema: {
|
|
5728
|
+
type: "object",
|
|
5729
|
+
properties: {
|
|
5730
|
+
orderId: { type: "string" },
|
|
5731
|
+
status: { type: "string" },
|
|
5732
|
+
checkoutUrl: { type: "string" }
|
|
5733
|
+
},
|
|
5734
|
+
required: ["orderId", "status", "checkoutUrl"]
|
|
5735
|
+
},
|
|
5736
|
+
handlerBody: `const orderId = createId('order');
|
|
5737
|
+
const quantity = typeof input.quantity === 'number' ? input.quantity : 1;
|
|
5738
|
+
const format = input.format ?? 'paperback';
|
|
5739
|
+
orderLog.push({
|
|
5740
|
+
orderId,
|
|
5741
|
+
bookId: objectId,
|
|
5742
|
+
format,
|
|
5743
|
+
quantity,
|
|
5744
|
+
userId: ctx.user.userId,
|
|
5745
|
+
});
|
|
5746
|
+
return {
|
|
5747
|
+
orderId,
|
|
5748
|
+
status: 'checkout_created',
|
|
5749
|
+
checkoutUrl: \`https://checkout.example/orders/\${orderId}\`,
|
|
5750
|
+
};`
|
|
5751
|
+
},
|
|
5752
|
+
{
|
|
5753
|
+
name: "follow",
|
|
5754
|
+
description: "Subscribe the current reader to updates about this author",
|
|
5755
|
+
className: "author",
|
|
5756
|
+
inputSchema: {
|
|
5757
|
+
type: "object",
|
|
5758
|
+
properties: {
|
|
5759
|
+
channel: { type: "string", description: "Notification channel such as email or push" }
|
|
5760
|
+
},
|
|
5761
|
+
required: ["channel"]
|
|
5762
|
+
},
|
|
5763
|
+
outputSchema: {
|
|
5764
|
+
type: "object",
|
|
5765
|
+
properties: {
|
|
5766
|
+
subscriptionId: { type: "string" },
|
|
5767
|
+
status: { type: "string" }
|
|
5768
|
+
},
|
|
5769
|
+
required: ["subscriptionId", "status"]
|
|
5770
|
+
},
|
|
5771
|
+
handlerBody: `const subscriptionId = createId('follow');
|
|
5772
|
+
followLog.push({
|
|
5773
|
+
subscriptionId,
|
|
5774
|
+
authorId: objectId,
|
|
5775
|
+
channel: input.channel,
|
|
5776
|
+
userId: ctx.user.userId,
|
|
5777
|
+
});
|
|
5778
|
+
return {
|
|
5779
|
+
subscriptionId,
|
|
5780
|
+
status: 'subscribed',
|
|
5781
|
+
};`
|
|
5782
|
+
},
|
|
5783
|
+
{
|
|
5784
|
+
name: "send_curated_list",
|
|
5785
|
+
description: "Send a curated list of books through an external email workflow",
|
|
5786
|
+
inputSchema: {
|
|
5787
|
+
type: "object",
|
|
5788
|
+
properties: {
|
|
5789
|
+
email: { type: "string", description: "Destination email address" },
|
|
5790
|
+
bookIds: { type: "array", items: { type: "string" }, description: "Books to include in the list" },
|
|
5791
|
+
message: { type: "string", description: "Optional note for the recipient" }
|
|
5792
|
+
},
|
|
5793
|
+
required: ["email", "bookIds"]
|
|
5794
|
+
},
|
|
5795
|
+
outputSchema: {
|
|
5796
|
+
type: "object",
|
|
5797
|
+
properties: {
|
|
5798
|
+
deliveryId: { type: "string" },
|
|
5799
|
+
status: { type: "string" }
|
|
5800
|
+
},
|
|
5801
|
+
required: ["deliveryId", "status"]
|
|
5802
|
+
},
|
|
5803
|
+
handlerBody: `const deliveryId = createId('delivery');
|
|
5804
|
+
deliveryLog.push({
|
|
5805
|
+
deliveryId,
|
|
5806
|
+
email: input.email,
|
|
5807
|
+
bookIds: Array.isArray(input.bookIds) ? input.bookIds : [],
|
|
5808
|
+
requestedBy: ctx.user.userId,
|
|
5809
|
+
});
|
|
5810
|
+
return {
|
|
5811
|
+
deliveryId,
|
|
5812
|
+
status: 'queued',
|
|
5813
|
+
};`
|
|
5814
|
+
}
|
|
5815
|
+
]
|
|
5816
|
+
};
|
|
5817
|
+
var SUPPORT_TEMPLATE = {
|
|
5818
|
+
id: "support",
|
|
5819
|
+
label: "Support Operations",
|
|
5820
|
+
summary: "A service graph that connects customers, orders, tickets, and agents.",
|
|
5821
|
+
manifestDescription: "A support operations graph that models customers, orders, tickets, agents, and external actions.",
|
|
5822
|
+
operations: [
|
|
5823
|
+
createClassOperation("customer", {
|
|
5824
|
+
name: { type: "string", description: "Customer display name" },
|
|
5825
|
+
email: { type: "email", description: "Customer email address" },
|
|
5826
|
+
segment: { type: "string", description: "Customer segment such as self-serve or enterprise" }
|
|
5827
|
+
}),
|
|
5828
|
+
createClassOperation("order", {
|
|
5829
|
+
number: { type: "string", description: "Order number shown to the customer" },
|
|
5830
|
+
total_eur: { type: "number", description: "Order total in euros" },
|
|
5831
|
+
status: { type: "string", description: "Current fulfillment status" }
|
|
5832
|
+
}),
|
|
5833
|
+
createClassOperation("ticket", {
|
|
5834
|
+
subject: { type: "string", description: "Support ticket subject" },
|
|
5835
|
+
priority: { type: "string", description: "Priority label" },
|
|
5836
|
+
channel: { type: "string", description: "Where the request came from" }
|
|
5837
|
+
}),
|
|
5838
|
+
createClassOperation("agent", {
|
|
5839
|
+
name: { type: "string", description: "Agent display name" },
|
|
5840
|
+
team: { type: "string", description: "Owning support team" },
|
|
5841
|
+
timezone: { type: "string", description: "Primary working timezone" }
|
|
5842
|
+
}),
|
|
5843
|
+
createRelationshipOperation({
|
|
5844
|
+
left: "customer",
|
|
5845
|
+
right: "order",
|
|
5846
|
+
leftSubmodel: "orders",
|
|
5847
|
+
rightSubmodel: "customer",
|
|
5848
|
+
leftIsMany: true,
|
|
5849
|
+
rightIsMany: false
|
|
5850
|
+
}),
|
|
5851
|
+
createRelationshipOperation({
|
|
5852
|
+
left: "customer",
|
|
5853
|
+
right: "ticket",
|
|
5854
|
+
leftSubmodel: "tickets",
|
|
5855
|
+
rightSubmodel: "customer",
|
|
5856
|
+
leftIsMany: true,
|
|
5857
|
+
rightIsMany: false
|
|
5858
|
+
}),
|
|
5859
|
+
createRelationshipOperation({
|
|
5860
|
+
left: "order",
|
|
5861
|
+
right: "ticket",
|
|
5862
|
+
leftSubmodel: "tickets",
|
|
5863
|
+
rightSubmodel: "order",
|
|
5864
|
+
leftIsMany: true,
|
|
5865
|
+
rightIsMany: false
|
|
5866
|
+
}),
|
|
5867
|
+
createRelationshipOperation({
|
|
5868
|
+
left: "agent",
|
|
5869
|
+
right: "ticket",
|
|
5870
|
+
leftSubmodel: "tickets",
|
|
5871
|
+
rightSubmodel: "assignee",
|
|
5872
|
+
leftIsMany: true,
|
|
5873
|
+
rightIsMany: false
|
|
5874
|
+
})
|
|
5875
|
+
],
|
|
5876
|
+
seedUser: {
|
|
5877
|
+
userId: "support_demo",
|
|
5878
|
+
name: "Taylor Support",
|
|
5879
|
+
email: "taylor@example.com",
|
|
5880
|
+
permissions: ["default"]
|
|
5881
|
+
},
|
|
5882
|
+
seedRecords: [
|
|
5883
|
+
{
|
|
5884
|
+
className: "customer",
|
|
5885
|
+
id: "acme",
|
|
5886
|
+
label: "Acme Studio",
|
|
5887
|
+
fields: {
|
|
5888
|
+
name: "Acme Studio",
|
|
5889
|
+
email: "ops@acme.example",
|
|
5890
|
+
segment: "enterprise"
|
|
5891
|
+
}
|
|
5892
|
+
},
|
|
5893
|
+
{
|
|
5894
|
+
className: "customer",
|
|
5895
|
+
id: "lumen",
|
|
5896
|
+
label: "Lumen Shop",
|
|
5897
|
+
fields: {
|
|
5898
|
+
name: "Lumen Shop",
|
|
5899
|
+
email: "hello@lumen.example",
|
|
5900
|
+
segment: "self-serve"
|
|
5901
|
+
}
|
|
5902
|
+
},
|
|
5903
|
+
{
|
|
5904
|
+
className: "agent",
|
|
5905
|
+
id: "nina",
|
|
5906
|
+
label: "Nina Patel",
|
|
5907
|
+
fields: {
|
|
5908
|
+
name: "Nina Patel",
|
|
5909
|
+
team: "Billing",
|
|
5910
|
+
timezone: "Europe/Paris"
|
|
5911
|
+
}
|
|
5912
|
+
},
|
|
5913
|
+
{
|
|
5914
|
+
className: "agent",
|
|
5915
|
+
id: "omar",
|
|
5916
|
+
label: "Omar Silva",
|
|
5917
|
+
fields: {
|
|
5918
|
+
name: "Omar Silva",
|
|
5919
|
+
team: "Logistics",
|
|
5920
|
+
timezone: "America/New_York"
|
|
5921
|
+
}
|
|
5922
|
+
},
|
|
5923
|
+
{
|
|
5924
|
+
className: "order",
|
|
5925
|
+
id: "ord_1001",
|
|
5926
|
+
label: "Order 1001",
|
|
5927
|
+
fields: {
|
|
5928
|
+
number: "1001",
|
|
5929
|
+
total_eur: 249,
|
|
5930
|
+
status: "processing"
|
|
5931
|
+
},
|
|
5932
|
+
relationships: {
|
|
5933
|
+
customer: "acme"
|
|
5934
|
+
}
|
|
5935
|
+
},
|
|
5936
|
+
{
|
|
5937
|
+
className: "order",
|
|
5938
|
+
id: "ord_1002",
|
|
5939
|
+
label: "Order 1002",
|
|
5940
|
+
fields: {
|
|
5941
|
+
number: "1002",
|
|
5942
|
+
total_eur: 78,
|
|
5943
|
+
status: "shipped"
|
|
5944
|
+
},
|
|
5945
|
+
relationships: {
|
|
5946
|
+
customer: "lumen"
|
|
5947
|
+
}
|
|
5948
|
+
},
|
|
5949
|
+
{
|
|
5950
|
+
className: "ticket",
|
|
5951
|
+
id: "ticket_401",
|
|
5952
|
+
label: "Missing invoice PDF",
|
|
5953
|
+
fields: {
|
|
5954
|
+
subject: "Missing invoice PDF",
|
|
5955
|
+
priority: "high",
|
|
5956
|
+
channel: "email"
|
|
5957
|
+
},
|
|
5958
|
+
relationships: {
|
|
5959
|
+
customer: "acme",
|
|
5960
|
+
order: "ord_1001",
|
|
5961
|
+
assignee: "nina"
|
|
5962
|
+
}
|
|
5963
|
+
},
|
|
5964
|
+
{
|
|
5965
|
+
className: "ticket",
|
|
5966
|
+
id: "ticket_402",
|
|
5967
|
+
label: "Delivery address update",
|
|
5968
|
+
fields: {
|
|
5969
|
+
subject: "Delivery address update",
|
|
5970
|
+
priority: "normal",
|
|
5971
|
+
channel: "chat"
|
|
5972
|
+
},
|
|
5973
|
+
relationships: {
|
|
5974
|
+
customer: "lumen",
|
|
5975
|
+
order: "ord_1002",
|
|
5976
|
+
assignee: "omar"
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
],
|
|
5980
|
+
runtimeSetup: [
|
|
5981
|
+
"const replyLog: Array<{ replyId: string; ticketId: string; message: string; userId: string }> = [];",
|
|
5982
|
+
"const refundLog: Array<{ refundId: string; orderId: string; amount: number; reason?: string; userId: string }> = [];",
|
|
5983
|
+
"const escalationLog: Array<{ escalationId: string; ticketId: string; queue: string; userId: string }> = [];",
|
|
5984
|
+
"const notificationLog: Array<{ notificationId: string; email: string; subject: string; userId: string }> = [];"
|
|
5985
|
+
],
|
|
5986
|
+
effects: [
|
|
5987
|
+
{
|
|
5988
|
+
name: "reply",
|
|
5989
|
+
description: "Post a support reply through the external CRM",
|
|
5990
|
+
className: "ticket",
|
|
5991
|
+
inputSchema: {
|
|
5992
|
+
type: "object",
|
|
5993
|
+
properties: {
|
|
5994
|
+
message: { type: "string", description: "Reply body sent to the customer" }
|
|
5995
|
+
},
|
|
5996
|
+
required: ["message"]
|
|
5997
|
+
},
|
|
5998
|
+
outputSchema: {
|
|
5999
|
+
type: "object",
|
|
6000
|
+
properties: {
|
|
6001
|
+
replyId: { type: "string" },
|
|
6002
|
+
status: { type: "string" }
|
|
6003
|
+
},
|
|
6004
|
+
required: ["replyId", "status"]
|
|
6005
|
+
},
|
|
6006
|
+
handlerBody: `const replyId = createId('reply');
|
|
6007
|
+
replyLog.push({
|
|
6008
|
+
replyId,
|
|
6009
|
+
ticketId: objectId,
|
|
6010
|
+
message: input.message,
|
|
6011
|
+
userId: ctx.user.userId,
|
|
6012
|
+
});
|
|
6013
|
+
return {
|
|
6014
|
+
replyId,
|
|
6015
|
+
status: 'sent',
|
|
6016
|
+
};`
|
|
6017
|
+
},
|
|
6018
|
+
{
|
|
6019
|
+
name: "refund",
|
|
6020
|
+
description: "Create a refund in the payment platform",
|
|
6021
|
+
className: "order",
|
|
6022
|
+
inputSchema: {
|
|
6023
|
+
type: "object",
|
|
6024
|
+
properties: {
|
|
6025
|
+
amount: { type: "number", description: "Refund amount in euros" },
|
|
6026
|
+
reason: { type: "string", description: "Reason shown in the payment platform" }
|
|
6027
|
+
},
|
|
6028
|
+
required: ["amount"]
|
|
6029
|
+
},
|
|
6030
|
+
outputSchema: {
|
|
6031
|
+
type: "object",
|
|
6032
|
+
properties: {
|
|
6033
|
+
refundId: { type: "string" },
|
|
6034
|
+
status: { type: "string" }
|
|
6035
|
+
},
|
|
6036
|
+
required: ["refundId", "status"]
|
|
6037
|
+
},
|
|
6038
|
+
handlerBody: `const refundId = createId('refund');
|
|
6039
|
+
refundLog.push({
|
|
6040
|
+
refundId,
|
|
6041
|
+
orderId: objectId,
|
|
6042
|
+
amount: input.amount,
|
|
6043
|
+
reason: input.reason,
|
|
6044
|
+
userId: ctx.user.userId,
|
|
6045
|
+
});
|
|
6046
|
+
return {
|
|
6047
|
+
refundId,
|
|
6048
|
+
status: 'queued',
|
|
6049
|
+
};`
|
|
6050
|
+
},
|
|
6051
|
+
{
|
|
6052
|
+
name: "escalate",
|
|
6053
|
+
description: "Escalate this ticket into another support queue",
|
|
6054
|
+
className: "ticket",
|
|
6055
|
+
inputSchema: {
|
|
6056
|
+
type: "object",
|
|
6057
|
+
properties: {
|
|
6058
|
+
queue: { type: "string", description: "Destination queue name" }
|
|
6059
|
+
},
|
|
6060
|
+
required: ["queue"]
|
|
6061
|
+
},
|
|
6062
|
+
outputSchema: {
|
|
6063
|
+
type: "object",
|
|
6064
|
+
properties: {
|
|
6065
|
+
escalationId: { type: "string" },
|
|
6066
|
+
status: { type: "string" }
|
|
6067
|
+
},
|
|
6068
|
+
required: ["escalationId", "status"]
|
|
6069
|
+
},
|
|
6070
|
+
handlerBody: `const escalationId = createId('escalation');
|
|
6071
|
+
escalationLog.push({
|
|
6072
|
+
escalationId,
|
|
6073
|
+
ticketId: objectId,
|
|
6074
|
+
queue: input.queue,
|
|
6075
|
+
userId: ctx.user.userId,
|
|
6076
|
+
});
|
|
6077
|
+
return {
|
|
6078
|
+
escalationId,
|
|
6079
|
+
status: 'forwarded',
|
|
6080
|
+
};`
|
|
6081
|
+
},
|
|
6082
|
+
{
|
|
6083
|
+
name: "notify_customer",
|
|
6084
|
+
description: "Send a customer notification through the outbound messaging system",
|
|
6085
|
+
inputSchema: {
|
|
6086
|
+
type: "object",
|
|
6087
|
+
properties: {
|
|
6088
|
+
email: { type: "string", description: "Destination email address" },
|
|
6089
|
+
subject: { type: "string", description: "Notification subject line" },
|
|
6090
|
+
message: { type: "string", description: "Notification body" }
|
|
6091
|
+
},
|
|
6092
|
+
required: ["email", "subject", "message"]
|
|
6093
|
+
},
|
|
6094
|
+
outputSchema: {
|
|
6095
|
+
type: "object",
|
|
6096
|
+
properties: {
|
|
6097
|
+
notificationId: { type: "string" },
|
|
6098
|
+
status: { type: "string" }
|
|
6099
|
+
},
|
|
6100
|
+
required: ["notificationId", "status"]
|
|
6101
|
+
},
|
|
6102
|
+
handlerBody: `const notificationId = createId('notification');
|
|
6103
|
+
notificationLog.push({
|
|
6104
|
+
notificationId,
|
|
6105
|
+
email: input.email,
|
|
6106
|
+
subject: input.subject,
|
|
6107
|
+
userId: ctx.user.userId,
|
|
6108
|
+
});
|
|
6109
|
+
return {
|
|
6110
|
+
notificationId,
|
|
6111
|
+
status: 'queued',
|
|
6112
|
+
};`
|
|
6113
|
+
}
|
|
6114
|
+
]
|
|
6115
|
+
};
|
|
6116
|
+
var DELIVERY_TEMPLATE = {
|
|
6117
|
+
id: "delivery",
|
|
6118
|
+
label: "Project Delivery",
|
|
6119
|
+
summary: "A planning graph for clients, projects, milestones, tasks, and delivery actions.",
|
|
6120
|
+
manifestDescription: "A project delivery graph that models clients, projects, milestones, tasks, owners, and external execution actions.",
|
|
6121
|
+
operations: [
|
|
6122
|
+
createClassOperation("client", {
|
|
6123
|
+
name: { type: "string", description: "Client or account name" },
|
|
6124
|
+
industry: { type: "string", description: "Primary industry" },
|
|
6125
|
+
region: { type: "string", description: "Operating region" }
|
|
6126
|
+
}),
|
|
6127
|
+
createClassOperation("project", {
|
|
6128
|
+
name: { type: "string", description: "Project name" },
|
|
6129
|
+
status: { type: "string", description: "Current delivery status" },
|
|
6130
|
+
budget_k_eur: { type: "number", description: "Budget in thousands of euros" }
|
|
6131
|
+
}),
|
|
6132
|
+
createClassOperation("milestone", {
|
|
6133
|
+
name: { type: "string", description: "Milestone title" },
|
|
6134
|
+
due_date: { type: "date", description: "Target due date" },
|
|
6135
|
+
status: { type: "string", description: "Milestone status" }
|
|
6136
|
+
}),
|
|
6137
|
+
createClassOperation("task", {
|
|
6138
|
+
title: { type: "string", description: "Task title" },
|
|
6139
|
+
status: { type: "string", description: "Task execution status" },
|
|
6140
|
+
estimate_days: { type: "number", description: "Estimated effort in days" }
|
|
6141
|
+
}),
|
|
6142
|
+
createClassOperation("owner", {
|
|
6143
|
+
name: { type: "string", description: "Owner full name" },
|
|
6144
|
+
role: { type: "string", description: "Role on the delivery team" },
|
|
6145
|
+
email: { type: "email", description: "Work email address" }
|
|
6146
|
+
}),
|
|
6147
|
+
createRelationshipOperation({
|
|
6148
|
+
left: "client",
|
|
6149
|
+
right: "project",
|
|
6150
|
+
leftSubmodel: "projects",
|
|
6151
|
+
rightSubmodel: "client",
|
|
6152
|
+
leftIsMany: true,
|
|
6153
|
+
rightIsMany: false
|
|
6154
|
+
}),
|
|
6155
|
+
createRelationshipOperation({
|
|
6156
|
+
left: "project",
|
|
6157
|
+
right: "milestone",
|
|
6158
|
+
leftSubmodel: "milestones",
|
|
6159
|
+
rightSubmodel: "project",
|
|
6160
|
+
leftIsMany: true,
|
|
6161
|
+
rightIsMany: false
|
|
6162
|
+
}),
|
|
6163
|
+
createRelationshipOperation({
|
|
6164
|
+
left: "milestone",
|
|
6165
|
+
right: "task",
|
|
6166
|
+
leftSubmodel: "tasks",
|
|
6167
|
+
rightSubmodel: "milestone",
|
|
6168
|
+
leftIsMany: true,
|
|
6169
|
+
rightIsMany: false
|
|
6170
|
+
}),
|
|
6171
|
+
createRelationshipOperation({
|
|
6172
|
+
left: "owner",
|
|
6173
|
+
right: "task",
|
|
6174
|
+
leftSubmodel: "tasks",
|
|
6175
|
+
rightSubmodel: "owner",
|
|
6176
|
+
leftIsMany: true,
|
|
6177
|
+
rightIsMany: false
|
|
6178
|
+
})
|
|
6179
|
+
],
|
|
6180
|
+
seedUser: {
|
|
6181
|
+
userId: "delivery_demo",
|
|
6182
|
+
name: "Alex Delivery",
|
|
6183
|
+
email: "alex@example.com",
|
|
6184
|
+
permissions: ["default"]
|
|
6185
|
+
},
|
|
6186
|
+
seedRecords: [
|
|
6187
|
+
{
|
|
6188
|
+
className: "client",
|
|
6189
|
+
id: "helios",
|
|
6190
|
+
label: "Helios Energy",
|
|
6191
|
+
fields: {
|
|
6192
|
+
name: "Helios Energy",
|
|
6193
|
+
industry: "Energy",
|
|
6194
|
+
region: "EMEA"
|
|
6195
|
+
}
|
|
6196
|
+
},
|
|
6197
|
+
{
|
|
6198
|
+
className: "owner",
|
|
6199
|
+
id: "lea",
|
|
6200
|
+
label: "Lea Martin",
|
|
6201
|
+
fields: {
|
|
6202
|
+
name: "Lea Martin",
|
|
6203
|
+
role: "Delivery lead",
|
|
6204
|
+
email: "lea@studio.example"
|
|
6205
|
+
}
|
|
6206
|
+
},
|
|
6207
|
+
{
|
|
6208
|
+
className: "owner",
|
|
6209
|
+
id: "sam",
|
|
6210
|
+
label: "Sam Ortega",
|
|
6211
|
+
fields: {
|
|
6212
|
+
name: "Sam Ortega",
|
|
6213
|
+
role: "Product engineer",
|
|
6214
|
+
email: "sam@studio.example"
|
|
6215
|
+
}
|
|
6216
|
+
},
|
|
6217
|
+
{
|
|
6218
|
+
className: "project",
|
|
6219
|
+
id: "helios_portal",
|
|
6220
|
+
label: "Helios Self-Service Portal",
|
|
6221
|
+
fields: {
|
|
6222
|
+
name: "Helios Self-Service Portal",
|
|
6223
|
+
status: "active",
|
|
6224
|
+
budget_k_eur: 180
|
|
6225
|
+
},
|
|
6226
|
+
relationships: {
|
|
6227
|
+
client: "helios"
|
|
6228
|
+
}
|
|
6229
|
+
},
|
|
6230
|
+
{
|
|
6231
|
+
className: "milestone",
|
|
6232
|
+
id: "beta_launch",
|
|
6233
|
+
label: "Beta launch",
|
|
6234
|
+
fields: {
|
|
6235
|
+
name: "Beta launch",
|
|
6236
|
+
due_date: "2026-05-15",
|
|
6237
|
+
status: "planned"
|
|
6238
|
+
},
|
|
6239
|
+
relationships: {
|
|
6240
|
+
project: "helios_portal"
|
|
6241
|
+
}
|
|
6242
|
+
},
|
|
6243
|
+
{
|
|
6244
|
+
className: "milestone",
|
|
6245
|
+
id: "ops_handoff",
|
|
6246
|
+
label: "Operations handoff",
|
|
6247
|
+
fields: {
|
|
6248
|
+
name: "Operations handoff",
|
|
6249
|
+
due_date: "2026-06-10",
|
|
6250
|
+
status: "planned"
|
|
6251
|
+
},
|
|
6252
|
+
relationships: {
|
|
6253
|
+
project: "helios_portal"
|
|
6254
|
+
}
|
|
6255
|
+
},
|
|
6256
|
+
{
|
|
6257
|
+
className: "task",
|
|
6258
|
+
id: "task_auth_flow",
|
|
6259
|
+
label: "Auth flow",
|
|
6260
|
+
fields: {
|
|
6261
|
+
title: "Finalize auth flow",
|
|
6262
|
+
status: "in_progress",
|
|
6263
|
+
estimate_days: 4
|
|
6264
|
+
},
|
|
6265
|
+
relationships: {
|
|
6266
|
+
milestone: "beta_launch",
|
|
6267
|
+
owner: "sam"
|
|
6268
|
+
}
|
|
6269
|
+
},
|
|
6270
|
+
{
|
|
6271
|
+
className: "task",
|
|
6272
|
+
id: "task_status_report",
|
|
6273
|
+
label: "Status report",
|
|
6274
|
+
fields: {
|
|
6275
|
+
title: "Prepare weekly status report",
|
|
6276
|
+
status: "todo",
|
|
6277
|
+
estimate_days: 1
|
|
6278
|
+
},
|
|
6279
|
+
relationships: {
|
|
6280
|
+
milestone: "ops_handoff",
|
|
6281
|
+
owner: "lea"
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
],
|
|
6285
|
+
runtimeSetup: [
|
|
6286
|
+
"const issueLog: Array<{ issueId: string; taskId: string; title: string; userId: string }> = [];",
|
|
6287
|
+
"const releaseLog: Array<{ releaseId: string; projectId: string; environment: string; userId: string }> = [];",
|
|
6288
|
+
"const reportLog: Array<{ reportId: string; email: string; projectId: string; userId: string }> = [];",
|
|
6289
|
+
"const pingLog: Array<{ pingId: string; ownerId: string; channel: string; userId: string }> = [];"
|
|
6290
|
+
],
|
|
6291
|
+
effects: [
|
|
6292
|
+
{
|
|
6293
|
+
name: "create_issue",
|
|
6294
|
+
description: "Create a delivery issue in the external tracker",
|
|
6295
|
+
className: "task",
|
|
6296
|
+
inputSchema: {
|
|
6297
|
+
type: "object",
|
|
6298
|
+
properties: {
|
|
6299
|
+
title: { type: "string", description: "Issue title" },
|
|
6300
|
+
description: { type: "string", description: "Issue details" }
|
|
6301
|
+
},
|
|
6302
|
+
required: ["title"]
|
|
6303
|
+
},
|
|
6304
|
+
outputSchema: {
|
|
6305
|
+
type: "object",
|
|
6306
|
+
properties: {
|
|
6307
|
+
issueId: { type: "string" },
|
|
6308
|
+
status: { type: "string" }
|
|
6309
|
+
},
|
|
6310
|
+
required: ["issueId", "status"]
|
|
6311
|
+
},
|
|
6312
|
+
handlerBody: `const issueId = createId('issue');
|
|
6313
|
+
issueLog.push({
|
|
6314
|
+
issueId,
|
|
6315
|
+
taskId: objectId,
|
|
6316
|
+
title: input.title,
|
|
6317
|
+
userId: ctx.user.userId,
|
|
6318
|
+
});
|
|
6319
|
+
return {
|
|
6320
|
+
issueId,
|
|
6321
|
+
status: 'created',
|
|
6322
|
+
};`
|
|
6323
|
+
},
|
|
6324
|
+
{
|
|
6325
|
+
name: "schedule_release",
|
|
6326
|
+
description: "Schedule a deployment for this project in the release system",
|
|
6327
|
+
className: "project",
|
|
6328
|
+
inputSchema: {
|
|
6329
|
+
type: "object",
|
|
6330
|
+
properties: {
|
|
6331
|
+
environment: { type: "string", description: "Target environment such as staging or production" }
|
|
6332
|
+
},
|
|
6333
|
+
required: ["environment"]
|
|
6334
|
+
},
|
|
6335
|
+
outputSchema: {
|
|
6336
|
+
type: "object",
|
|
6337
|
+
properties: {
|
|
6338
|
+
releaseId: { type: "string" },
|
|
6339
|
+
status: { type: "string" }
|
|
6340
|
+
},
|
|
6341
|
+
required: ["releaseId", "status"]
|
|
6342
|
+
},
|
|
6343
|
+
handlerBody: `const releaseId = createId('release');
|
|
6344
|
+
releaseLog.push({
|
|
6345
|
+
releaseId,
|
|
6346
|
+
projectId: objectId,
|
|
6347
|
+
environment: input.environment,
|
|
6348
|
+
userId: ctx.user.userId,
|
|
6349
|
+
});
|
|
6350
|
+
return {
|
|
6351
|
+
releaseId,
|
|
6352
|
+
status: 'scheduled',
|
|
6353
|
+
};`
|
|
6354
|
+
},
|
|
6355
|
+
{
|
|
6356
|
+
name: "ping",
|
|
6357
|
+
description: "Send a follow-up to the owner in an external messaging channel",
|
|
6358
|
+
className: "owner",
|
|
6359
|
+
inputSchema: {
|
|
6360
|
+
type: "object",
|
|
6361
|
+
properties: {
|
|
6362
|
+
channel: { type: "string", description: "Destination channel such as slack or email" },
|
|
6363
|
+
message: { type: "string", description: "Message body" }
|
|
6364
|
+
},
|
|
6365
|
+
required: ["channel", "message"]
|
|
6366
|
+
},
|
|
6367
|
+
outputSchema: {
|
|
6368
|
+
type: "object",
|
|
6369
|
+
properties: {
|
|
6370
|
+
pingId: { type: "string" },
|
|
6371
|
+
status: { type: "string" }
|
|
6372
|
+
},
|
|
6373
|
+
required: ["pingId", "status"]
|
|
6374
|
+
},
|
|
6375
|
+
handlerBody: `const pingId = createId('ping');
|
|
6376
|
+
pingLog.push({
|
|
6377
|
+
pingId,
|
|
6378
|
+
ownerId: objectId,
|
|
6379
|
+
channel: input.channel,
|
|
6380
|
+
userId: ctx.user.userId,
|
|
6381
|
+
});
|
|
6382
|
+
return {
|
|
6383
|
+
pingId,
|
|
6384
|
+
status: 'sent',
|
|
6385
|
+
};`
|
|
6386
|
+
},
|
|
6387
|
+
{
|
|
6388
|
+
name: "send_status_report",
|
|
6389
|
+
description: "Send a status report to stakeholders through an external workflow",
|
|
6390
|
+
inputSchema: {
|
|
6391
|
+
type: "object",
|
|
6392
|
+
properties: {
|
|
6393
|
+
email: { type: "string", description: "Destination email address" },
|
|
6394
|
+
projectId: { type: "string", description: "Project referenced by the report" },
|
|
6395
|
+
summary: { type: "string", description: "Status report summary" }
|
|
6396
|
+
},
|
|
6397
|
+
required: ["email", "projectId", "summary"]
|
|
6398
|
+
},
|
|
6399
|
+
outputSchema: {
|
|
6400
|
+
type: "object",
|
|
6401
|
+
properties: {
|
|
6402
|
+
reportId: { type: "string" },
|
|
6403
|
+
status: { type: "string" }
|
|
6404
|
+
},
|
|
6405
|
+
required: ["reportId", "status"]
|
|
6406
|
+
},
|
|
6407
|
+
handlerBody: `const reportId = createId('report');
|
|
6408
|
+
reportLog.push({
|
|
6409
|
+
reportId,
|
|
6410
|
+
email: input.email,
|
|
6411
|
+
projectId: input.projectId,
|
|
6412
|
+
userId: ctx.user.userId,
|
|
6413
|
+
});
|
|
6414
|
+
return {
|
|
6415
|
+
reportId,
|
|
6416
|
+
status: 'queued',
|
|
6417
|
+
};`
|
|
6418
|
+
}
|
|
6419
|
+
]
|
|
6420
|
+
};
|
|
6421
|
+
var STARTER_TEMPLATES = {
|
|
6422
|
+
library: LIBRARY_TEMPLATE,
|
|
6423
|
+
support: SUPPORT_TEMPLATE,
|
|
6424
|
+
delivery: DELIVERY_TEMPLATE
|
|
6425
|
+
};
|
|
6426
|
+
function toWsUrl(apiUrl) {
|
|
6427
|
+
return apiUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
6428
|
+
}
|
|
6429
|
+
function formatValue(value) {
|
|
6430
|
+
return JSON.stringify(value, null, 2);
|
|
6431
|
+
}
|
|
6432
|
+
function indent(text, spaces) {
|
|
6433
|
+
const pad = " ".repeat(spaces);
|
|
6434
|
+
return text.split("\n").map((line) => line.length > 0 ? `${pad}${line}` : line).join("\n");
|
|
6435
|
+
}
|
|
6436
|
+
function renderEffectRegistration(effect) {
|
|
6437
|
+
const staticLine = effect.static ? "\n static: true," : "";
|
|
6438
|
+
const classLine = effect.className ? `
|
|
6439
|
+
className: '${effect.className}',` : "";
|
|
6440
|
+
const inputSchema = indent(formatValue(effect.inputSchema), 6);
|
|
6441
|
+
const outputSchema = indent(formatValue(effect.outputSchema), 6);
|
|
6442
|
+
const handlerSignature = effect.className && !effect.static ? "async (objectId: string, input: any, ctx: any) =>" : "async (input: any, ctx: any) =>";
|
|
6443
|
+
return ` {
|
|
6444
|
+
name: '${effect.name}',
|
|
6445
|
+
description: ${JSON.stringify(effect.description)},${classLine}${staticLine}
|
|
6446
|
+
inputSchema: ${inputSchema.trimStart()},
|
|
6447
|
+
outputSchema: ${outputSchema.trimStart()},
|
|
6448
|
+
handler: ${handlerSignature} {
|
|
6449
|
+
${indent(effect.handlerBody, 8)}
|
|
6450
|
+
},
|
|
6451
|
+
}`;
|
|
6452
|
+
}
|
|
6453
|
+
function listStarterTemplates() {
|
|
6454
|
+
return Object.values(STARTER_TEMPLATES);
|
|
6455
|
+
}
|
|
6456
|
+
function isStarterTemplateId(value) {
|
|
6457
|
+
return value in STARTER_TEMPLATES;
|
|
6458
|
+
}
|
|
6459
|
+
function getStarterTemplate(templateId) {
|
|
6460
|
+
return STARTER_TEMPLATES[templateId || DEFAULT_STARTER_TEMPLATE_ID];
|
|
6461
|
+
}
|
|
6462
|
+
function createStarterManifest(name, templateId) {
|
|
6463
|
+
const template = getStarterTemplate(templateId);
|
|
6464
|
+
return {
|
|
6465
|
+
schemaVersion: 2,
|
|
6466
|
+
name,
|
|
6467
|
+
description: `${name} \u2014 ${template.manifestDescription}`,
|
|
6468
|
+
volumes: [{
|
|
6469
|
+
name: "schema",
|
|
6470
|
+
scope: "sandbox",
|
|
6471
|
+
imports: [
|
|
6472
|
+
{ alias: "@std", name: "standard_modules", label: "prod" }
|
|
6473
|
+
],
|
|
6474
|
+
operations: [
|
|
6475
|
+
...template.operations,
|
|
6476
|
+
...template.effects.map(createEffectOperation)
|
|
6477
|
+
]
|
|
6478
|
+
}]
|
|
6479
|
+
};
|
|
6480
|
+
}
|
|
6481
|
+
function generateSeedScript(sandboxId, apiUrl, templateId) {
|
|
6482
|
+
const template = getStarterTemplate(templateId);
|
|
6483
|
+
const wsUrl = toWsUrl(apiUrl);
|
|
6484
|
+
const recordsLiteral = indent(formatValue(template.seedRecords), 2);
|
|
6485
|
+
return `/**
|
|
6486
|
+
* Seed script for the "${template.label}" starter template.
|
|
6487
|
+
*
|
|
6488
|
+
* Safe to re-run:
|
|
6489
|
+
* - \`recordObject()\` upserts by \`{ className, id }\`
|
|
6490
|
+
* - relationships are re-applied on each run
|
|
6491
|
+
*/
|
|
6492
|
+
|
|
6493
|
+
import { Granular } from '@granular-software/sdk';
|
|
6494
|
+
|
|
6495
|
+
const SANDBOX_ID = '${sandboxId}';
|
|
6496
|
+
const API_URL = '${wsUrl}';
|
|
6497
|
+
|
|
6498
|
+
function requireApiKey(): string {
|
|
6499
|
+
const apiKey = process.env.GRANULAR_API_KEY;
|
|
6500
|
+
if (!apiKey) {
|
|
6501
|
+
throw new Error('Set GRANULAR_API_KEY in .env.local or in your shell before running this script.');
|
|
6502
|
+
}
|
|
6503
|
+
return apiKey;
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
const records = ${recordsLiteral.trimStart()};
|
|
6507
|
+
|
|
6508
|
+
async function main() {
|
|
6509
|
+
const granular = new Granular({
|
|
6510
|
+
apiKey: requireApiKey(),
|
|
6511
|
+
apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
|
|
6512
|
+
});
|
|
6513
|
+
|
|
6514
|
+
// Connect as one app user so the SDK can create an environment for the seed run.
|
|
6515
|
+
const env = await granular.connect({
|
|
6516
|
+
sandbox: SANDBOX_ID,
|
|
6517
|
+
userId: '${template.seedUser.userId}',
|
|
6518
|
+
name: '${template.seedUser.name}',
|
|
6519
|
+
email: '${template.seedUser.email}',
|
|
6520
|
+
permissions: ${formatValue(template.seedUser.permissions)},
|
|
6521
|
+
});
|
|
6522
|
+
|
|
6523
|
+
try {
|
|
6524
|
+
for (const record of records) {
|
|
6525
|
+
await env.recordObject(record);
|
|
6526
|
+
console.log(\`Upserted \${record.className}:\${record.id}\`);
|
|
6527
|
+
}
|
|
6528
|
+
} finally {
|
|
6529
|
+
await env.disconnect();
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
console.log(\`Seeded \${records.length} records into \${SANDBOX_ID}\`);
|
|
6533
|
+
}
|
|
6534
|
+
|
|
6535
|
+
main().catch((error) => {
|
|
6536
|
+
console.error('[Seed] Failed:', error);
|
|
6537
|
+
process.exit(1);
|
|
6538
|
+
});
|
|
6539
|
+
`;
|
|
6540
|
+
}
|
|
6541
|
+
function generateEffectsScript(sandboxId, apiUrl, templateId) {
|
|
6542
|
+
const template = getStarterTemplate(templateId);
|
|
6543
|
+
const wsUrl = toWsUrl(apiUrl);
|
|
6544
|
+
const runtimeSetup = template.runtimeSetup.map((line) => `${line}`).join("\n");
|
|
6545
|
+
const registrations = template.effects.map(renderEffectRegistration).join(",\n");
|
|
6546
|
+
return `/**
|
|
6547
|
+
* Live effect host for the "${template.label}" starter template.
|
|
6548
|
+
*
|
|
6549
|
+
* These handlers simulate calls to external systems such as checkout, CRM,
|
|
6550
|
+
* messaging, or release tooling. Replace the stub bodies with your real APIs.
|
|
6551
|
+
*/
|
|
6552
|
+
|
|
6553
|
+
import { Granular } from '@granular-software/sdk';
|
|
6554
|
+
|
|
6555
|
+
const SANDBOX_ID = '${sandboxId}';
|
|
6556
|
+
const API_URL = '${wsUrl}';
|
|
6557
|
+
|
|
6558
|
+
${runtimeSetup}
|
|
6559
|
+
|
|
6560
|
+
function requireApiKey(): string {
|
|
6561
|
+
const apiKey = process.env.GRANULAR_API_KEY;
|
|
6562
|
+
if (!apiKey) {
|
|
6563
|
+
throw new Error('Set GRANULAR_API_KEY in .env.local or in your shell before running this script.');
|
|
6564
|
+
}
|
|
6565
|
+
return apiKey;
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
function createId(prefix: string): string {
|
|
6569
|
+
return \`\${prefix}_\${Math.random().toString(36).slice(2, 10)}\`;
|
|
6570
|
+
}
|
|
6571
|
+
|
|
6572
|
+
async function main() {
|
|
6573
|
+
const granular = new Granular({
|
|
6574
|
+
apiKey: requireApiKey(),
|
|
6575
|
+
apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
|
|
6576
|
+
});
|
|
6577
|
+
|
|
6578
|
+
await granular.registerEffects(SANDBOX_ID, [
|
|
6579
|
+
${registrations}
|
|
6580
|
+
]);
|
|
6581
|
+
|
|
6582
|
+
console.log(\`Effect host connected for \${SANDBOX_ID}\`);
|
|
6583
|
+
console.log('Handlers are live. Press Ctrl+C to disconnect.');
|
|
6584
|
+
|
|
6585
|
+
// Keep one active timer so Node or Bun does not exit while the host is serving effects.
|
|
6586
|
+
const keepAliveTimer = setInterval(() => {}, 60_000);
|
|
6587
|
+
|
|
6588
|
+
await new Promise<void>((resolve) => {
|
|
6589
|
+
let shuttingDown = false;
|
|
6590
|
+
|
|
6591
|
+
const shutdown = async (signal: string) => {
|
|
6592
|
+
if (shuttingDown) return;
|
|
6593
|
+
shuttingDown = true;
|
|
6594
|
+
clearInterval(keepAliveTimer);
|
|
6595
|
+
console.log(\`Shutting down after \${signal}...\`);
|
|
6596
|
+
await granular.disconnectEffects(SANDBOX_ID);
|
|
6597
|
+
resolve();
|
|
6598
|
+
process.exit(0);
|
|
6599
|
+
};
|
|
6600
|
+
|
|
6601
|
+
process.once('SIGINT', () => { void shutdown('SIGINT'); });
|
|
6602
|
+
process.once('SIGTERM', () => { void shutdown('SIGTERM'); });
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6606
|
+
main().catch((error) => {
|
|
6607
|
+
console.error('[Effects] Failed:', error);
|
|
6608
|
+
process.exit(1);
|
|
6609
|
+
});
|
|
6610
|
+
`;
|
|
6611
|
+
}
|
|
6612
|
+
|
|
5437
6613
|
// src/cli/config.ts
|
|
5438
6614
|
var MANIFEST_FILE = "granular.json";
|
|
5439
6615
|
var RC_FILE = ".granularrc";
|
|
@@ -5552,162 +6728,11 @@ function ensureGitignore() {
|
|
|
5552
6728
|
fs__namespace.writeFileSync(gitignorePath, content + section, "utf-8");
|
|
5553
6729
|
}
|
|
5554
6730
|
}
|
|
5555
|
-
function
|
|
5556
|
-
const operations = classNames.flatMap((className) => {
|
|
5557
|
-
const searchName = `search_${className}s`;
|
|
5558
|
-
return [
|
|
5559
|
-
{
|
|
5560
|
-
withEffect: {
|
|
5561
|
-
name: "get_info",
|
|
5562
|
-
description: `Get details of a ${className} by ID`,
|
|
5563
|
-
attachedClass: className,
|
|
5564
|
-
isStatic: false,
|
|
5565
|
-
inputSchema: {
|
|
5566
|
-
type: "object",
|
|
5567
|
-
properties: {
|
|
5568
|
-
include_related: { type: "boolean", description: "Include related items" }
|
|
5569
|
-
}
|
|
5570
|
-
},
|
|
5571
|
-
outputSchema: {
|
|
5572
|
-
type: "object",
|
|
5573
|
-
properties: {
|
|
5574
|
-
id: { type: "string" },
|
|
5575
|
-
name: { type: "string" },
|
|
5576
|
-
related: { type: "array", items: { type: "object" } }
|
|
5577
|
-
}
|
|
5578
|
-
}
|
|
5579
|
-
}
|
|
5580
|
-
},
|
|
5581
|
-
{
|
|
5582
|
-
withEffect: {
|
|
5583
|
-
name: searchName,
|
|
5584
|
-
description: `Search ${className}s by keyword`,
|
|
5585
|
-
attachedClass: className,
|
|
5586
|
-
isStatic: true,
|
|
5587
|
-
inputSchema: {
|
|
5588
|
-
type: "object",
|
|
5589
|
-
properties: {
|
|
5590
|
-
query: { type: "string", description: "Search keyword" },
|
|
5591
|
-
limit: { type: "number", description: "Max results" }
|
|
5592
|
-
},
|
|
5593
|
-
required: ["query"]
|
|
5594
|
-
},
|
|
5595
|
-
outputSchema: {
|
|
5596
|
-
type: "object",
|
|
5597
|
-
properties: {
|
|
5598
|
-
query: { type: "string" },
|
|
5599
|
-
results: {
|
|
5600
|
-
type: "array",
|
|
5601
|
-
items: {
|
|
5602
|
-
type: "object",
|
|
5603
|
-
properties: {
|
|
5604
|
-
id: { type: "string" },
|
|
5605
|
-
name: { type: "string" }
|
|
5606
|
-
}
|
|
5607
|
-
}
|
|
5608
|
-
},
|
|
5609
|
-
total: { type: "number" }
|
|
5610
|
-
}
|
|
5611
|
-
}
|
|
5612
|
-
}
|
|
5613
|
-
}
|
|
5614
|
-
];
|
|
5615
|
-
});
|
|
5616
|
-
operations.push({
|
|
5617
|
-
withEffect: {
|
|
5618
|
-
name: "full_text_search",
|
|
5619
|
-
description: "Search across all types",
|
|
5620
|
-
inputSchema: {
|
|
5621
|
-
type: "object",
|
|
5622
|
-
properties: {
|
|
5623
|
-
query: { type: "string", description: "Search query" },
|
|
5624
|
-
types: { type: "array", items: { type: "string" }, description: "Filter by type" }
|
|
5625
|
-
},
|
|
5626
|
-
required: ["query"]
|
|
5627
|
-
},
|
|
5628
|
-
outputSchema: {
|
|
5629
|
-
type: "object",
|
|
5630
|
-
properties: {
|
|
5631
|
-
query: { type: "string" },
|
|
5632
|
-
types: { type: "array", items: { type: "string" } },
|
|
5633
|
-
matches: {
|
|
5634
|
-
type: "array",
|
|
5635
|
-
items: {
|
|
5636
|
-
type: "object",
|
|
5637
|
-
properties: {
|
|
5638
|
-
type: { type: "string" },
|
|
5639
|
-
id: { type: "string" },
|
|
5640
|
-
label: { type: "string" },
|
|
5641
|
-
snippet: { type: "string" }
|
|
5642
|
-
}
|
|
5643
|
-
}
|
|
5644
|
-
},
|
|
5645
|
-
total: { type: "number" }
|
|
5646
|
-
}
|
|
5647
|
-
}
|
|
5648
|
-
}
|
|
5649
|
-
});
|
|
5650
|
-
return operations;
|
|
5651
|
-
}
|
|
5652
|
-
function createDefaultManifest(name) {
|
|
5653
|
-
const classNames = ["note", "tag"];
|
|
6731
|
+
function createDefaultManifest(name, templateId = DEFAULT_STARTER_TEMPLATE_ID) {
|
|
5654
6732
|
return {
|
|
5655
|
-
manifest:
|
|
5656
|
-
schemaVersion: 2,
|
|
5657
|
-
name,
|
|
5658
|
-
description: `${name} \u2014 built with Granular`,
|
|
5659
|
-
volumes: [{
|
|
5660
|
-
name: "schema",
|
|
5661
|
-
scope: "sandbox",
|
|
5662
|
-
imports: [
|
|
5663
|
-
{ alias: "@std", name: "standard_modules", label: "prod" }
|
|
5664
|
-
],
|
|
5665
|
-
operations: [
|
|
5666
|
-
{
|
|
5667
|
-
create: "note",
|
|
5668
|
-
extends: "@std/class",
|
|
5669
|
-
has: {
|
|
5670
|
-
title: { type: "string", description: "Note title" },
|
|
5671
|
-
content: { type: "string", description: "Note content" },
|
|
5672
|
-
priority: { type: "number", description: "Priority level (1-5)" }
|
|
5673
|
-
}
|
|
5674
|
-
},
|
|
5675
|
-
{
|
|
5676
|
-
create: "tag",
|
|
5677
|
-
extends: "@std/class",
|
|
5678
|
-
has: {
|
|
5679
|
-
name: { type: "string", description: "Tag name" },
|
|
5680
|
-
color: { type: "string", description: "Tag color hex code" }
|
|
5681
|
-
}
|
|
5682
|
-
},
|
|
5683
|
-
{
|
|
5684
|
-
defineRelationship: {
|
|
5685
|
-
left: "note",
|
|
5686
|
-
right: "tag",
|
|
5687
|
-
leftSubmodel: "tags",
|
|
5688
|
-
rightSubmodel: "notes",
|
|
5689
|
-
leftIsMany: true,
|
|
5690
|
-
rightIsMany: true
|
|
5691
|
-
}
|
|
5692
|
-
},
|
|
5693
|
-
...createDefaultEffectOperations(classNames)
|
|
5694
|
-
]
|
|
5695
|
-
}]
|
|
5696
|
-
}
|
|
6733
|
+
manifest: createStarterManifest(name, templateId)
|
|
5697
6734
|
};
|
|
5698
6735
|
}
|
|
5699
|
-
var STD_CLASS_NAMES = /* @__PURE__ */ new Set(["entity", "class", "user", "company", "string", "number", "boolean", "tool_parameter"]);
|
|
5700
|
-
function getClassNamesFromManifest(manifest) {
|
|
5701
|
-
const names = /* @__PURE__ */ new Set();
|
|
5702
|
-
for (const vol of manifest.volumes ?? []) {
|
|
5703
|
-
for (const op of vol.operations ?? []) {
|
|
5704
|
-
if (op.create && !STD_CLASS_NAMES.has(op.create)) {
|
|
5705
|
-
names.add(op.create);
|
|
5706
|
-
}
|
|
5707
|
-
}
|
|
5708
|
-
}
|
|
5709
|
-
return Array.from(names);
|
|
5710
|
-
}
|
|
5711
6736
|
function resolveConfig(options) {
|
|
5712
6737
|
const apiKey = loadApiKey();
|
|
5713
6738
|
const apiUrl = loadApiUrl();
|
|
@@ -7310,11 +8335,11 @@ var Ora = class {
|
|
|
7310
8335
|
get indent() {
|
|
7311
8336
|
return this.#indent;
|
|
7312
8337
|
}
|
|
7313
|
-
set indent(
|
|
7314
|
-
if (!(
|
|
8338
|
+
set indent(indent2 = 0) {
|
|
8339
|
+
if (!(indent2 >= 0 && Number.isInteger(indent2))) {
|
|
7315
8340
|
throw new Error("The `indent` option must be an integer from 0 and up");
|
|
7316
8341
|
}
|
|
7317
|
-
this.#indent =
|
|
8342
|
+
this.#indent = indent2;
|
|
7318
8343
|
this.#updateLineCount();
|
|
7319
8344
|
}
|
|
7320
8345
|
get interval() {
|
|
@@ -7636,174 +8661,6 @@ function buildStatus(status) {
|
|
|
7636
8661
|
}
|
|
7637
8662
|
|
|
7638
8663
|
// src/cli/commands/init.ts
|
|
7639
|
-
var EFFECTS_SCRIPT_NAME = "granular-effects.ts";
|
|
7640
|
-
function toWsUrl(apiUrl) {
|
|
7641
|
-
return apiUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
7642
|
-
}
|
|
7643
|
-
function generateEffectsScript(sandboxId, apiUrl, classNames) {
|
|
7644
|
-
const wsUrl = toWsUrl(apiUrl);
|
|
7645
|
-
classNames.length > 0;
|
|
7646
|
-
const mockDataEntries = classNames.map(
|
|
7647
|
-
(c) => ` ${c}: [
|
|
7648
|
-
{ id: '${c}_001', name: 'Sample ${c}' },
|
|
7649
|
-
{ id: '${c}_002', name: 'Another ${c}' },
|
|
7650
|
-
]`
|
|
7651
|
-
);
|
|
7652
|
-
const mockDataBlock = mockDataEntries.length > 0 ? `const MOCK_DATA: Record<string, any[]> = {
|
|
7653
|
-
${mockDataEntries.join(",\n")},
|
|
7654
|
-
};` : "const MOCK_DATA: Record<string, any[]> = {};";
|
|
7655
|
-
const instanceEffects = classNames.map((className) => {
|
|
7656
|
-
className.charAt(0).toUpperCase() + className.slice(1);
|
|
7657
|
-
return ` {
|
|
7658
|
-
name: 'get_info',
|
|
7659
|
-
description: \`Get details of a ${className} by ID\`,
|
|
7660
|
-
className: '${className}',
|
|
7661
|
-
inputSchema: {
|
|
7662
|
-
type: 'object',
|
|
7663
|
-
properties: { include_related: { type: 'boolean', description: 'Include related items' } },
|
|
7664
|
-
},
|
|
7665
|
-
handler: async (id: string, params: any) => {
|
|
7666
|
-
logEffect('get_info', '${className}', id, params);
|
|
7667
|
-
const item = (MOCK_DATA['${className}'] || []).find((x: any) => x.id === id);
|
|
7668
|
-
if (!item) return { error: \`${className} "\${id}" not found\` };
|
|
7669
|
-
return { ...item, ...(params?.include_related ? { related: [] } : {}) };
|
|
7670
|
-
},
|
|
7671
|
-
}`;
|
|
7672
|
-
});
|
|
7673
|
-
const staticEffects = classNames.map((className) => {
|
|
7674
|
-
const searchName = "search_" + className + "s";
|
|
7675
|
-
return ` {
|
|
7676
|
-
name: '${searchName}',
|
|
7677
|
-
description: \`Search ${className}s by keyword\`,
|
|
7678
|
-
className: '${className}',
|
|
7679
|
-
static: true,
|
|
7680
|
-
inputSchema: {
|
|
7681
|
-
type: 'object',
|
|
7682
|
-
properties: {
|
|
7683
|
-
query: { type: 'string', description: 'Search keyword' },
|
|
7684
|
-
limit: { type: 'number', description: 'Max results' },
|
|
7685
|
-
},
|
|
7686
|
-
required: ['query'],
|
|
7687
|
-
},
|
|
7688
|
-
handler: async (params: any) => {
|
|
7689
|
-
logEffect('${searchName}', null, null, params);
|
|
7690
|
-
const q = ((params?.query) || '').toLowerCase();
|
|
7691
|
-
const limit = params?.limit ?? 10;
|
|
7692
|
-
const list = (MOCK_DATA['${className}'] || []).filter((x: any) =>
|
|
7693
|
-
String(x.name || x.id || '').toLowerCase().includes(q)
|
|
7694
|
-
).slice(0, limit);
|
|
7695
|
-
return { query: params?.query, results: list, total: list.length };
|
|
7696
|
-
},
|
|
7697
|
-
}`;
|
|
7698
|
-
});
|
|
7699
|
-
const allEffects = [...instanceEffects, ...staticEffects];
|
|
7700
|
-
if (allEffects.length > 0) {
|
|
7701
|
-
allEffects.push(` {
|
|
7702
|
-
name: 'full_text_search',
|
|
7703
|
-
description: 'Search across all types',
|
|
7704
|
-
inputSchema: {
|
|
7705
|
-
type: 'object',
|
|
7706
|
-
properties: {
|
|
7707
|
-
query: { type: 'string', description: 'Search query' },
|
|
7708
|
-
types: { type: 'array', items: { type: 'string' }, description: 'Filter by type' },
|
|
7709
|
-
},
|
|
7710
|
-
required: ['query'],
|
|
7711
|
-
},
|
|
7712
|
-
handler: async (params: any) => {
|
|
7713
|
-
logEffect('full_text_search', null, null, params);
|
|
7714
|
-
const q = ((params?.query) || '').toLowerCase();
|
|
7715
|
-
const types: string[] = params?.types || [${classNames.map((c) => `'${c}'`).join(", ")}];
|
|
7716
|
-
const matches: any[] = [];
|
|
7717
|
-
for (const type of types) {
|
|
7718
|
-
const items = MOCK_DATA[type] || [];
|
|
7719
|
-
for (const item of items) {
|
|
7720
|
-
const text = Object.values(item).join(' ').toLowerCase();
|
|
7721
|
-
if (!q || text.includes(q)) {
|
|
7722
|
-
matches.push({ type, id: item.id, label: item.name || item.id, snippet: text.slice(0, 60) });
|
|
7723
|
-
}
|
|
7724
|
-
}
|
|
7725
|
-
}
|
|
7726
|
-
return { query: params?.query, types, matches, total: matches.length };
|
|
7727
|
-
},
|
|
7728
|
-
}`);
|
|
7729
|
-
}
|
|
7730
|
-
const effectsArray = allEffects.length > 0 ? `[
|
|
7731
|
-
${allEffects.join(",\n")}
|
|
7732
|
-
]` : "[]";
|
|
7733
|
-
return `/**
|
|
7734
|
-
* Granular effects script for sandbox ${sandboxId}
|
|
7735
|
-
* Run with: npx tsx ${EFFECTS_SCRIPT_NAME}
|
|
7736
|
-
* Requires: GRANULAR_API_KEY in env or .env.local, and optional "ws" package for Node.
|
|
7737
|
-
*
|
|
7738
|
-
* Keeps the process alive so the simulator can invoke these effect handlers.
|
|
7739
|
-
*/
|
|
7740
|
-
|
|
7741
|
-
import { Granular } from '@granular-software/sdk';
|
|
7742
|
-
|
|
7743
|
-
const SANDBOX_ID = '${sandboxId}';
|
|
7744
|
-
const API_URL = '${wsUrl}';
|
|
7745
|
-
|
|
7746
|
-
function log(msg: string) {
|
|
7747
|
-
console.log(\`[\${new Date().toISOString()}] [Effects] \${msg}\`);
|
|
7748
|
-
}
|
|
7749
|
-
|
|
7750
|
-
function logEffect(name: string, className: string | null, id: string | null, params: any) {
|
|
7751
|
-
const target = className ? (id ? \`\${className}(\${id})\` : \`\${className}\`) : 'global';
|
|
7752
|
-
console.log(\` [EFFECT] \${name}(\${target}) \${JSON.stringify(params || {})}\`);
|
|
7753
|
-
}
|
|
7754
|
-
|
|
7755
|
-
async function main() {
|
|
7756
|
-
const auth = process.env.GRANULAR_TOKEN ?? process.env.GRANULAR_API_KEY;
|
|
7757
|
-
if (!auth) {
|
|
7758
|
-
console.error('[Effects] Set GRANULAR_API_KEY or GRANULAR_TOKEN (e.g. from simulator "Copy CLI env").');
|
|
7759
|
-
process.exit(1);
|
|
7760
|
-
}
|
|
7761
|
-
|
|
7762
|
-
log('Initializing client...');
|
|
7763
|
-
const granular = new Granular({
|
|
7764
|
-
...(auth.startsWith('eyJ') ? { token: auth } : { apiKey: auth }),
|
|
7765
|
-
apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
|
|
7766
|
-
});
|
|
7767
|
-
|
|
7768
|
-
log(\`Registering live effects for sandbox \${SANDBOX_ID}\`);
|
|
7769
|
-
${mockDataBlock}
|
|
7770
|
-
|
|
7771
|
-
await granular.registerEffects(SANDBOX_ID, ${effectsArray});
|
|
7772
|
-
|
|
7773
|
-
log('Effects registered. Process kept alive for simulator. Press Ctrl+C to exit.');
|
|
7774
|
-
|
|
7775
|
-
const keepAliveTimer = setInterval(() => {
|
|
7776
|
-
// Keep an active event-loop handle so Bun/Node does not exit immediately.
|
|
7777
|
-
}, 60_000);
|
|
7778
|
-
|
|
7779
|
-
await new Promise<void>((resolve) => {
|
|
7780
|
-
let shuttingDown = false;
|
|
7781
|
-
|
|
7782
|
-
const shutdown = async (signal: string) => {
|
|
7783
|
-
if (shuttingDown) return;
|
|
7784
|
-
shuttingDown = true;
|
|
7785
|
-
clearInterval(keepAliveTimer);
|
|
7786
|
-
log(\`Received \${signal}. Shutting down effects host...\`);
|
|
7787
|
-
try {
|
|
7788
|
-
await granular.disconnectEffects(SANDBOX_ID);
|
|
7789
|
-
} catch (error) {
|
|
7790
|
-
console.warn('[Effects] Failed to disconnect live effects cleanly:', error);
|
|
7791
|
-
}
|
|
7792
|
-
resolve();
|
|
7793
|
-
process.exit(0);
|
|
7794
|
-
};
|
|
7795
|
-
|
|
7796
|
-
process.once('SIGINT', () => { void shutdown('SIGINT'); });
|
|
7797
|
-
process.once('SIGTERM', () => { void shutdown('SIGTERM'); });
|
|
7798
|
-
});
|
|
7799
|
-
}
|
|
7800
|
-
|
|
7801
|
-
main().catch((err) => {
|
|
7802
|
-
console.error('[Effects] Failed:', err);
|
|
7803
|
-
process.exit(1);
|
|
7804
|
-
});
|
|
7805
|
-
`;
|
|
7806
|
-
}
|
|
7807
8664
|
function prompt(question, defaultValue) {
|
|
7808
8665
|
const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
|
|
7809
8666
|
const suffix = defaultValue ? ` ${brand.muted(`(${defaultValue})`)}` : "";
|
|
@@ -7833,6 +8690,40 @@ function confirm(question, defaultYes = true) {
|
|
|
7833
8690
|
return answer.toLowerCase().startsWith("y");
|
|
7834
8691
|
});
|
|
7835
8692
|
}
|
|
8693
|
+
async function selectStarterTemplate(requestedTemplate) {
|
|
8694
|
+
if (requestedTemplate) {
|
|
8695
|
+
if (!isStarterTemplateId(requestedTemplate)) {
|
|
8696
|
+
throw new Error(
|
|
8697
|
+
`Unknown template "${requestedTemplate}". Use one of: ${listStarterTemplates().map((template) => template.id).join(", ")}.`
|
|
8698
|
+
);
|
|
8699
|
+
}
|
|
8700
|
+
return requestedTemplate;
|
|
8701
|
+
}
|
|
8702
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
8703
|
+
return DEFAULT_STARTER_TEMPLATE_ID;
|
|
8704
|
+
}
|
|
8705
|
+
const templates = listStarterTemplates();
|
|
8706
|
+
const defaultIndex = templates.findIndex((template) => template.id === DEFAULT_STARTER_TEMPLATE_ID);
|
|
8707
|
+
step("Choose a starter ontology template");
|
|
8708
|
+
for (const [index, template] of templates.entries()) {
|
|
8709
|
+
const isDefault = template.id === DEFAULT_STARTER_TEMPLATE_ID;
|
|
8710
|
+
const defaultLabel = isDefault ? ` ${brand.muted("(default)")}` : "";
|
|
8711
|
+
console.log(` ${index + 1}. ${brand.bold(template.label)}${defaultLabel}`);
|
|
8712
|
+
dim(` ${template.id} \u2014 ${template.summary}`);
|
|
8713
|
+
}
|
|
8714
|
+
console.log();
|
|
8715
|
+
const rawChoice = await prompt("Template", String(defaultIndex + 1));
|
|
8716
|
+
const normalizedChoice = rawChoice.trim().toLowerCase();
|
|
8717
|
+
const byNumber = Number.parseInt(normalizedChoice, 10);
|
|
8718
|
+
if (Number.isFinite(byNumber) && byNumber >= 1 && byNumber <= templates.length) {
|
|
8719
|
+
return templates[byNumber - 1].id;
|
|
8720
|
+
}
|
|
8721
|
+
if (isStarterTemplateId(normalizedChoice)) {
|
|
8722
|
+
return normalizedChoice;
|
|
8723
|
+
}
|
|
8724
|
+
warn(`Unknown template "${rawChoice}". Falling back to ${DEFAULT_STARTER_TEMPLATE_ID}.`);
|
|
8725
|
+
return DEFAULT_STARTER_TEMPLATE_ID;
|
|
8726
|
+
}
|
|
7836
8727
|
async function initCommand(projectName, options) {
|
|
7837
8728
|
printHeader();
|
|
7838
8729
|
if (manifestExists()) {
|
|
@@ -7886,6 +8777,9 @@ async function initCommand(projectName, options) {
|
|
|
7886
8777
|
dim("Saved to .env.local");
|
|
7887
8778
|
const dirName = path__namespace.basename(process.cwd());
|
|
7888
8779
|
const name = projectName || await prompt("Project name", dirName);
|
|
8780
|
+
const templateId = await selectStarterTemplate(options?.template);
|
|
8781
|
+
const template = getStarterTemplate(templateId);
|
|
8782
|
+
info(`Using template: ${template.label}`);
|
|
7889
8783
|
console.log();
|
|
7890
8784
|
const creating = spinner(`Creating sandbox "${name}"...`);
|
|
7891
8785
|
let sandbox;
|
|
@@ -7898,9 +8792,9 @@ async function initCommand(projectName, options) {
|
|
|
7898
8792
|
creating.fail(` Failed to create sandbox: ${err.message}`);
|
|
7899
8793
|
process.exit(1);
|
|
7900
8794
|
}
|
|
7901
|
-
const project = createDefaultManifest(name);
|
|
8795
|
+
const project = createDefaultManifest(name, templateId);
|
|
7902
8796
|
writeManifestFile(project);
|
|
7903
|
-
success(`Created ${brand.bold("granular.json")} with starter
|
|
8797
|
+
success(`Created ${brand.bold("granular.json")} with the ${template.label} starter ontology`);
|
|
7904
8798
|
writeRcFile({
|
|
7905
8799
|
sandboxId: sandbox.sandboxId,
|
|
7906
8800
|
sandboxName: sandbox.name,
|
|
@@ -7932,11 +8826,20 @@ async function initCommand(projectName, options) {
|
|
|
7932
8826
|
dim("You can retry with `granular build`");
|
|
7933
8827
|
}
|
|
7934
8828
|
}
|
|
7935
|
-
const classNames = getClassNamesFromManifest(project.manifest);
|
|
7936
8829
|
const effectsPath = path__namespace.join(getProjectRoot(), EFFECTS_SCRIPT_NAME);
|
|
7937
|
-
const
|
|
7938
|
-
fs__namespace.writeFileSync(
|
|
7939
|
-
|
|
8830
|
+
const seedPath = path__namespace.join(getProjectRoot(), SEED_SCRIPT_NAME);
|
|
8831
|
+
fs__namespace.writeFileSync(
|
|
8832
|
+
effectsPath,
|
|
8833
|
+
generateEffectsScript(sandbox.sandboxId, apiUrl, templateId),
|
|
8834
|
+
"utf-8"
|
|
8835
|
+
);
|
|
8836
|
+
success(`Created ${brand.bold(EFFECTS_SCRIPT_NAME)} (external effect host)`);
|
|
8837
|
+
fs__namespace.writeFileSync(
|
|
8838
|
+
seedPath,
|
|
8839
|
+
generateSeedScript(sandbox.sandboxId, apiUrl, templateId),
|
|
8840
|
+
"utf-8"
|
|
8841
|
+
);
|
|
8842
|
+
success(`Created ${brand.bold(SEED_SCRIPT_NAME)} (sample record seeding script)`);
|
|
7940
8843
|
console.log();
|
|
7941
8844
|
divider();
|
|
7942
8845
|
console.log();
|
|
@@ -7944,21 +8847,29 @@ async function initCommand(projectName, options) {
|
|
|
7944
8847
|
console.log();
|
|
7945
8848
|
keyValue({
|
|
7946
8849
|
"Sandbox": sandbox.sandboxId,
|
|
8850
|
+
"Template": template.label,
|
|
7947
8851
|
"Manifest": "granular.json",
|
|
7948
8852
|
"Config": ".granularrc",
|
|
7949
8853
|
"API Key": ".env.local",
|
|
8854
|
+
"Seed script": SEED_SCRIPT_NAME,
|
|
7950
8855
|
"Effects script": EFFECTS_SCRIPT_NAME
|
|
7951
8856
|
});
|
|
7952
|
-
|
|
7953
|
-
{ command: "granular
|
|
7954
|
-
{ command:
|
|
7955
|
-
{ command:
|
|
7956
|
-
|
|
8857
|
+
const nextSteps2 = options?.skipBuild ? [
|
|
8858
|
+
{ command: "granular build", description: "Build the starter ontology before you seed or simulate" },
|
|
8859
|
+
{ command: `npx tsx ${SEED_SCRIPT_NAME}`, description: "Push the sample records into your sandbox" },
|
|
8860
|
+
{ command: `npx tsx ${EFFECTS_SCRIPT_NAME}`, description: "Connect live handlers for the declared external actions" },
|
|
8861
|
+
{ command: "granular simulate", description: "Open the simulator for this sandbox" }
|
|
8862
|
+
] : [
|
|
8863
|
+
{ command: `npx tsx ${SEED_SCRIPT_NAME}`, description: "Push the sample records into your sandbox" },
|
|
8864
|
+
{ command: `npx tsx ${EFFECTS_SCRIPT_NAME}`, description: "Connect live handlers for the declared external actions" },
|
|
8865
|
+
{ command: "granular simulate", description: "Open the simulator for this sandbox" },
|
|
8866
|
+
{ command: "granular add class <name>", description: "Extend the ontology once the starter model is clear" }
|
|
8867
|
+
];
|
|
8868
|
+
nextSteps(nextSteps2);
|
|
7957
8869
|
console.log();
|
|
7958
|
-
dim("
|
|
7959
|
-
|
|
7960
|
-
dim("
|
|
7961
|
-
hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
|
|
8870
|
+
dim("Mental model:");
|
|
8871
|
+
dim(" Query and navigate data through the ontology. Use effects only for actions that leave the sandbox.");
|
|
8872
|
+
dim(" The seed script shows how records enter the graph. The effects script shows how external systems plug in.");
|
|
7962
8873
|
console.log();
|
|
7963
8874
|
}
|
|
7964
8875
|
var DEFAULT_LOCAL_API_KEY = "gn_sk_tenant_default_principal_local_e2e_00000000";
|
|
@@ -8619,7 +9530,7 @@ ${manifest.description}`);
|
|
|
8619
9530
|
lines.push(`import { Granular } from '@granular-software/sdk';`);
|
|
8620
9531
|
lines.push(`
|
|
8621
9532
|
const granular = new Granular({`);
|
|
8622
|
-
lines.push(`
|
|
9533
|
+
lines.push(` apiKey: process.env.GRANULAR_API_KEY!,`);
|
|
8623
9534
|
lines.push(`});`);
|
|
8624
9535
|
lines.push(`
|
|
8625
9536
|
// 1. Connect to the sandbox for one of your app users`);
|
|
@@ -8678,7 +9589,7 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8678
9589
|
}
|
|
8679
9590
|
lines.push(`
|
|
8680
9591
|
### 3. Declare And Register Effects`);
|
|
8681
|
-
lines.push(`Declare effects in your manifest with \`withEffect\`, then register live handlers at sandbox scope.`);
|
|
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.`);
|
|
8682
9593
|
lines.push(`
|
|
8683
9594
|
\`\`\`typescript`);
|
|
8684
9595
|
lines.push(`await granular.registerEffects('your-sandbox-id', [`);
|
|
@@ -8686,35 +9597,22 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8686
9597
|
const cls = classes[0];
|
|
8687
9598
|
lines.push(` // Instance method on ${cls.name}`);
|
|
8688
9599
|
lines.push(` {`);
|
|
8689
|
-
lines.push(` name: '
|
|
8690
|
-
lines.push(` description: '
|
|
9600
|
+
lines.push(` name: 'sync_external_action',`);
|
|
9601
|
+
lines.push(` description: 'Perform an external action for this ${cls.name}',`);
|
|
8691
9602
|
lines.push(` className: '${cls.name}',`);
|
|
8692
|
-
lines.push(` inputSchema: { type: 'object', properties: {
|
|
8693
|
-
lines.push(` outputSchema: { type: 'object', properties: {
|
|
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'] },`);
|
|
8694
9605
|
lines.push(` handler: async (id, params, ctx) => {`);
|
|
8695
9606
|
lines.push(` // 'id' is the real-world ID of the ${cls.name}`);
|
|
8696
9607
|
lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
|
|
8697
|
-
lines.push(` return {
|
|
8698
|
-
lines.push(` },`);
|
|
8699
|
-
lines.push(` },`);
|
|
8700
|
-
lines.push(` // Static method on ${cls.name}`);
|
|
8701
|
-
lines.push(` {`);
|
|
8702
|
-
lines.push(` name: 'search',`);
|
|
8703
|
-
lines.push(` description: 'Search ${cls.name}s',`);
|
|
8704
|
-
lines.push(` className: '${cls.name}',`);
|
|
8705
|
-
lines.push(` static: true,`);
|
|
8706
|
-
lines.push(` inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },`);
|
|
8707
|
-
lines.push(` outputSchema: { type: 'object', properties: { ids: { type: 'array' } }, required: ['ids'] },`);
|
|
8708
|
-
lines.push(` handler: async (params, ctx) => {`);
|
|
8709
|
-
lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
|
|
8710
|
-
lines.push(` return { ids: [] };`);
|
|
9608
|
+
lines.push(` return { actionId: \`act_\${id}\`, status: 'queued' };`);
|
|
8711
9609
|
lines.push(` },`);
|
|
8712
9610
|
lines.push(` },`);
|
|
8713
9611
|
}
|
|
8714
9612
|
lines.push(` // Global effect`);
|
|
8715
9613
|
lines.push(` {`);
|
|
8716
|
-
lines.push(` name: '
|
|
8717
|
-
lines.push(` description: 'Send
|
|
9614
|
+
lines.push(` name: 'notify_team',`);
|
|
9615
|
+
lines.push(` description: 'Send a message through an external workflow',`);
|
|
8718
9616
|
lines.push(` inputSchema: { type: 'object', properties: { msg: { type: 'string' } }, required: ['msg'] },`);
|
|
8719
9617
|
lines.push(` handler: async (params, ctx) => {`);
|
|
8720
9618
|
lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
|
|
@@ -8731,9 +9629,9 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8731
9629
|
lines.push(`const job = await env.submitJob(\``);
|
|
8732
9630
|
const classImports = classes.map((c) => c.name.charAt(0).toUpperCase() + c.name.slice(1)).join(", ");
|
|
8733
9631
|
if (classImports) {
|
|
8734
|
-
lines.push(` import { ${classImports},
|
|
9632
|
+
lines.push(` import { ${classImports}, notify_team } from './sandbox-tools';`);
|
|
8735
9633
|
} else {
|
|
8736
|
-
lines.push(` import {
|
|
9634
|
+
lines.push(` import { notify_team } from './sandbox-tools';`);
|
|
8737
9635
|
}
|
|
8738
9636
|
lines.push(``);
|
|
8739
9637
|
if (classes.length > 0) {
|
|
@@ -8746,16 +9644,13 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8746
9644
|
lines.push(``);
|
|
8747
9645
|
lines.push(` // 2. Call instance method`);
|
|
8748
9646
|
lines.push(` if (item) {`);
|
|
8749
|
-
lines.push(` const
|
|
8750
|
-
lines.push(` console.log(
|
|
9647
|
+
lines.push(` const action = await item.sync_external_action({ reason: 'Example workflow' });`);
|
|
9648
|
+
lines.push(` console.log(action);`);
|
|
8751
9649
|
lines.push(` }`);
|
|
8752
9650
|
lines.push(``);
|
|
8753
|
-
lines.push(` // 3. Call static method`);
|
|
8754
|
-
lines.push(` const results = await ${ClassName}.search({ query: 'test' });`);
|
|
8755
|
-
lines.push(``);
|
|
8756
9651
|
}
|
|
8757
9652
|
lines.push(` // Call global tool`);
|
|
8758
|
-
lines.push(` await
|
|
9653
|
+
lines.push(` await notify_team({ msg: 'Job completed' });`);
|
|
8759
9654
|
lines.push(``);
|
|
8760
9655
|
lines.push(` return { success: true };`);
|
|
8761
9656
|
lines.push(`\`);`);
|
|
@@ -8817,9 +9712,12 @@ program2.hook("preAction", () => {
|
|
|
8817
9712
|
process.env.GRANULAR_ENDPOINT_MODE = mode;
|
|
8818
9713
|
}
|
|
8819
9714
|
});
|
|
8820
|
-
program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").action(async (projectName, opts) => {
|
|
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) => {
|
|
8821
9716
|
try {
|
|
8822
|
-
await initCommand(projectName, {
|
|
9717
|
+
await initCommand(projectName, {
|
|
9718
|
+
skipBuild: opts.skipBuild,
|
|
9719
|
+
template: opts.template
|
|
9720
|
+
});
|
|
8823
9721
|
} catch (err) {
|
|
8824
9722
|
error(err.message);
|
|
8825
9723
|
process.exit(1);
|