@fungi.computer/trellis 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,126 @@
1
- # Temporary Holding Version
1
+ # Trellis (`@fungi.computer/trellis`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Trellis builds and runs workflows: typed graphs of your Apps' methods, started
4
+ by hand, on a schedule or by an event.
5
+
6
+ ![The Trellis editor with the Bonsai checks workflow: Workflow input, a Change script node and a Bonsai Checks.report step on the canvas, between the method Palette and the Inspector](docs/media/editor.png)
7
+
8
+ Trellis is one of the starter Apps of
9
+ [Oyster](https://github.com/fungi-computer/oyster), the open App host that runs
10
+ on your own Cloudflare account. On Oyster, each run executes on Cloudflare
11
+ Workflows, and its steps call the same App methods your agents use.
12
+
13
+ ## What you can do
14
+
15
+ - Drag methods from the **Palette** onto the **Canvas**, wire their typed ports,
16
+ and set inputs in the **Inspector**. Trellis checks the graph as you edit and
17
+ marks it **Valid** when it can run.
18
+ - Add review steps, waits, conditions, repeats and maps, or a **Code** node that
19
+ runs your own JavaScript in a sandbox with a time limit.
20
+ - **Publish** an immutable version, then **Run** it with inputs. Drafts never
21
+ run.
22
+ - Follow a run in **Runs**: each step's state, outputs, duration and cost, with
23
+ image and video previews. Approve or reject review steps there; only a person
24
+ can answer a review.
25
+ - Arm a published version in **Triggers** to start on a schedule, on an App
26
+ event such as a Bonsai push or merge, or from a webhook, and have it report
27
+ its ending to one of your chats.
28
+ - Start from **Templates**: Bonsai checks (run `npm test` on each Change and
29
+ report the result), Build on merge, Review a result, Storyboard (panels from
30
+ Prints with a review per beat) and Storyboard + Save to git.
31
+ - **Export**, **Import** and **Fork** workflows as JSON.
32
+
33
+ <img alt="The Trellis editor in the light theme, with a Bonsai Repos.list step and a Workflow input on the canvas" src="docs/media/editor-light.png">
34
+
35
+ ## Get it
36
+
37
+ Trellis installs with the other starter Apps. On a deployed Oyster (see the
38
+ [Oyster quick start](https://github.com/fungi-computer/oyster#quick-start-deploy-your-own-oyster)),
39
+ run:
40
+
41
+ ```sh
42
+ export OYSTER_URL=https://oyster.<your-subdomain>.workers.dev
43
+ export OYSTER_OWNER_KEY=<your owner key>
44
+ npx oyster install-apps
45
+ ```
46
+
47
+ The installer clones this repository and the six others, seeds each on your
48
+ Oyster with `git push`, builds them on your Computer and installs them. Sign in
49
+ at `$OYSTER_URL/login` with the owner key and open Trellis from the Tourtois
50
+ desktop. Run `install-apps` again to update; each App keeps its data.
51
+
52
+ ## Change it
53
+
54
+ Every installed App is also a template you can fork on your own Oyster. Fork
55
+ Trellis into a Bonsai repository and clone it with ordinary `git`:
56
+
57
+ ```sh
58
+ curl -X POST -H "Authorization: Bearer $OYSTER_OWNER_KEY" -H 'content-type: application/json' \
59
+ -d '{"appId":"app_trellis","name":"my-trellis"}' \
60
+ $OYSTER_URL/apps/bonsai/methods/apps.fork
61
+ git -c http.extraHeader="Authorization: Bearer $OYSTER_OWNER_KEY" \
62
+ clone $OYSTER_URL/git/bonsai/my-trellis.git
63
+ ```
64
+
65
+ The reply names your fork's App id and repository. Push your edits, open the
66
+ repository in Bonsai and choose **Settings**, then **Build & install** with that
67
+ App id and your commit as `git:<sha>`. Install the Release at `trellis` to
68
+ replace the running Trellis, or at a new id to keep both.
69
+
70
+ To work from GitHub instead, fork this repository and point the installer at
71
+ your forks with `--repositories <base URL>` and `--ref <branch or tag>`. The
72
+ installer reads all seven starter Apps from that base, so fork them under one
73
+ account.
74
+
75
+ ## How workflows are stored
76
+
77
+ Drafts live in Trellis's own SQLite database and save as you edit. Publishing
78
+ writes `workflows/<id>.json` to the installation's `trellis` git repository and
79
+ tags it `<id>@v<n>`, so every version that ever ran stays readable. Pick an old
80
+ version in the version menu and choose **Restore as draft** to edit it again. A
81
+ run records the exact version it started from, and Oyster checks the caller's
82
+ permissions again before each step.
83
+
84
+ ## Agent tools
85
+
86
+ Agents call Trellis through Oyster's MCP endpoint, or at
87
+ `POST $OYSTER_URL/apps/trellis/methods/<name>` with a key; see
88
+ [Git, keys and MCP](https://github.com/fungi-computer/oyster#git-keys-and-mcp).
89
+
90
+ | Method | What it does |
91
+ | ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
92
+ | `catalog` | Lists your installed Apps' methods, or one App's full port types. |
93
+ | `listWorkflows` / `loadWorkflow` | Lists the library, or reads one workflow. |
94
+ | `saveDraft` | Saves a draft, with a revision check so concurrent edits never clobber each other. |
95
+ | `publishWorkflow` / `listVersions` / `revertDraft` | Publishes a version, lists versions, or restores one as a draft. |
96
+ | `runWorkflow` | Runs a published version, optionally reporting the ending to a chat. |
97
+ | `listRuns` / `runStatus` / `cancel` | Lists runs, reads one run's steps and reviews, or cancels it. |
98
+ | `arm` / `armEventTrigger` and their `disarm` pairs | Arms or disarms a schedule or an App-event trigger. |
99
+ | `exportWorkflow` / `importWorkflow` / `forkWorkflow` | Moves workflows in and out as JSON, or copies one. |
100
+
101
+ `answerReview` exists too, but only a person can answer a review.
102
+
103
+ ## Build and check
104
+
105
+ Trellis is a React App built with Vite. The canvas uses React Flow, the panes
106
+ [Maosaico](https://github.com/fungi-computer/maosaico) and the controls
107
+ [Caps](https://github.com/fungi-computer/caps).
108
+
109
+ ```sh
110
+ pnpm install
111
+ pnpm build
112
+ pnpm typecheck
113
+ pnpm test
114
+ ```
115
+
116
+ [src/editor/](src/editor/) is the graph editor, [src/runs/](src/runs/) the run
117
+ view and reviews, and [src/triggers/](src/triggers/) the trigger page.
118
+ [backend/library.ts](backend/library.ts) owns drafts and publication, and
119
+ [templates/](templates/) holds the bundled templates as JSON. The agent tools
120
+ are in [backend/methods.ts](backend/methods.ts) and
121
+ [backend/library-methods.ts](backend/library-methods.ts), and
122
+ [package.json](package.json) declares the permissions the App asks for.
123
+
124
+ ## License
125
+
126
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,58 @@
1
+ import * as S from "effect/Schema";
2
+ import { catalogSchema, type Catalog } from "@fungi.computer/braid";
3
+ import type { WorkflowMethodContext } from "./methods.js";
4
+ import { catalogReplySchema } from "@fungi.computer/workflows-wire";
5
+ export { catalogReplySchema } from "@fungi.computer/workflows-wire";
6
+
7
+ /** The unique App ids a definition's node bindings actually name. */
8
+ export function definitionAppIds(definition: {
9
+ readonly apps: Readonly<Record<string, { readonly appId: string }>>;
10
+ }): string[] {
11
+ return [...new Set(Object.values(definition.apps).map((app) => app.appId))];
12
+ }
13
+
14
+ /**
15
+ * The host selects member or lifecycle authority; the App never invents
16
+ * grants. Reads the palette index once to resolve installations, then reads
17
+ * each named App's full ports only -- never the whole installed set -- so a
18
+ * large installed-App count never grows this reply with ports no caller asked
19
+ * for.
20
+ */
21
+ export async function readWorkflowCatalog(
22
+ workflows: WorkflowMethodContext["env"]["WORKFLOWS"],
23
+ appIds: readonly string[],
24
+ ): Promise<Catalog | null> {
25
+ const needed = new Set(appIds);
26
+ // No App is named: skip the read entirely, not just the port detail --
27
+ // never requiring the WORKFLOWS binding just to save an App-free draft.
28
+ if (needed.size === 0) return {};
29
+ if (!workflows) return null;
30
+ const index = catalogReplySchema.parse(await workflows.catalog({}));
31
+ if (!("apps" in index)) return null;
32
+ const matches = index.apps.filter((app) => needed.has(app.appId));
33
+ const entries = await Promise.all(
34
+ matches.map(async (match) => {
35
+ const full = catalogReplySchema.parse(
36
+ await workflows.catalog({ installationId: match.installationId }),
37
+ );
38
+ return "app" in full ? full.app : null;
39
+ }),
40
+ );
41
+ return S.decodeUnknownSync(catalogSchema)(
42
+ Object.fromEntries(
43
+ entries.flatMap((app) =>
44
+ app === null
45
+ ? []
46
+ : [
47
+ [
48
+ app.appId,
49
+ app.events === undefined
50
+ ? { methods: app.methods }
51
+ : { methods: app.methods, events: app.events },
52
+ ] as const,
53
+ ],
54
+ ),
55
+ ),
56
+ { onExcessProperty: "error" },
57
+ );
58
+ }
@@ -0,0 +1,56 @@
1
+ import { defineAppGill, type AppBackendGill } from "@fungi.computer/agar/gills";
2
+ import { libraryListing } from "@fungi.computer/workflows-wire";
3
+
4
+ type GillRow = ReturnType<AppBackendGill["rows"]>["rows"][number];
5
+
6
+ /** The installation's saved library supplies one page, without a Git rebuild. */
7
+ export const workflowLibraryGill = defineAppGill({
8
+ id: "workflow-library",
9
+ title: "Workflow Library",
10
+ source: "listWorkflows",
11
+ limit: 20,
12
+ sizes: ["M", "L"],
13
+ input: ({ limit, after }): ReturnType<AppBackendGill["input"]> =>
14
+ after === undefined ? { limit } : { limit, after },
15
+ rows: (output) => {
16
+ const identity = { viewId: "workflow-library", title: "Workflow Library" };
17
+ const page = libraryListing.parse(output);
18
+ if ("kind" in page)
19
+ return {
20
+ ...identity,
21
+ state: "error",
22
+ rows: [],
23
+ error:
24
+ "The saved Workflow library isn't ready. Open Trellis to update it.",
25
+ recovery: { kind: "open" },
26
+ };
27
+ if (page.items.length === 0)
28
+ return { ...identity, state: "empty", rows: [] };
29
+ const rows = page.items.map((item): GillRow => {
30
+ const row: GillRow = {
31
+ id: item.workflowId,
32
+ title: item.title.trim() || "Untitled Workflow",
33
+ status:
34
+ item.published === null
35
+ ? {
36
+ label: item.draftRevision === null ? "Not published" : "Draft",
37
+ tone: "neutral",
38
+ }
39
+ : {
40
+ label:
41
+ item.published.version === null
42
+ ? "Published"
43
+ : `Published v${String(item.published.version)}`,
44
+ tone: "success",
45
+ },
46
+ open: { params: [{ key: "workflow", value: item.workflowId }] },
47
+ };
48
+ return item.draftRevision === null
49
+ ? row
50
+ : { ...row, detail: "Draft available" };
51
+ });
52
+ return page.next === undefined
53
+ ? { ...identity, state: "full", rows }
54
+ : { ...identity, state: "full", rows, next: page.next };
55
+ },
56
+ });
@@ -0,0 +1,40 @@
1
+ import type { AppGitPushFact } from "@fungi.computer/agar/git";
2
+ import { DurableObject } from "cloudflare:workers";
3
+ import { appBackend, type WorkflowMethodContext } from "./methods.js";
4
+ import { readWorkflowCatalog } from "./catalog.js";
5
+ import { onWorkflowGitPush } from "./push.js";
6
+ import { createWorkflowIndexes } from "./indexes.js";
7
+ import {
8
+ backgroundSyncRefusalSchema,
9
+ type BackgroundSyncRefusal,
10
+ } from "@fungi.computer/puffball/background-replay";
11
+
12
+ class WorkflowFacet extends DurableObject<WorkflowMethodContext["env"]> {
13
+ declare public readonly env: WorkflowMethodContext["env"];
14
+ readonly indexes;
15
+ constructor(ctx: DurableObjectState, env: WorkflowMethodContext["env"]) {
16
+ super(ctx, env);
17
+ this.indexes = createWorkflowIndexes(ctx.storage);
18
+ }
19
+ async onGitPush(fact: AppGitPushFact): Promise<void> {
20
+ if (!this.env.GIT) throw new Error("Git access is not granted");
21
+ await onWorkflowGitPush(
22
+ {
23
+ git: this.env.GIT,
24
+ indexes: this.indexes,
25
+ catalog: (appIds) => readWorkflowCatalog(this.env.WORKFLOWS, appIds),
26
+ },
27
+ fact,
28
+ );
29
+ }
30
+ async syncRefused(input: BackgroundSyncRefusal): Promise<void> {
31
+ await this.indexes.publicationJobs.refused(
32
+ backgroundSyncRefusalSchema.parse(input),
33
+ );
34
+ }
35
+ override fetch(): Promise<Response> {
36
+ return Promise.resolve(new Response(null, { status: 405 }));
37
+ }
38
+ }
39
+
40
+ export const Trellis = appBackend.implement(WorkflowFacet);