@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 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 createDefaultEffectOperations(classNames) {
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(indent = 0) {
7314
- if (!(indent >= 0 && Number.isInteger(indent))) {
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 = 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 manifest`);
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 effectsContent = generateEffectsScript(sandbox.sandboxId, apiUrl, classNames);
7938
- fs__namespace.writeFileSync(effectsPath, effectsContent, "utf-8");
7939
- success(`Created ${brand.bold(EFFECTS_SCRIPT_NAME)} (effects script for simulator)`);
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
- nextSteps([
7953
- { command: "granular add class <name>", description: "Add a class to your schema" },
7954
- { command: "granular build", description: "Build your sandbox" },
7955
- { command: "granular status", description: "Check build status" }
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("Run the effects script (keeps process alive for simulator):");
7959
- hint(`npx tsx ${EFFECTS_SCRIPT_NAME}`, "requires tsx and GRANULAR_API_KEY in .env.local");
7960
- dim("Open the simulator in your browser:");
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(` ...(process.env.GRANULAR_TOKEN ? { token: process.env.GRANULAR_TOKEN } : { apiKey: process.env.GRANULAR_API_KEY! }),`);
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: 'summarize',`);
8690
- lines.push(` description: 'Summarize this ${cls.name}',`);
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: { maxLength: { type: 'number' } } },`);
8693
- lines.push(` outputSchema: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] },`);
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 { summary: \`Summary for \${id}\` };`);
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: 'notify',`);
8717
- lines.push(` description: 'Send notification',`);
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}, notify } from './sandbox-tools';`);
9632
+ lines.push(` import { ${classImports}, notify_team } from './sandbox-tools';`);
8735
9633
  } else {
8736
- lines.push(` import { notify } from './sandbox-tools';`);
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 summary = await item.summarize({ maxLength: 100 });`);
8750
- lines.push(` console.log(summary);`);
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 notify({ msg: 'Job completed' });`);
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, { skipBuild: opts.skipBuild });
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);