@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 +21 -0
- package/README.md +125 -2
- package/backend/catalog.ts +58 -0
- package/backend/gills.ts +56 -0
- package/backend/index.ts +40 -0
- package/backend/indexes.ts +419 -0
- package/backend/library-methods.ts +287 -0
- package/backend/library.ts +594 -0
- package/backend/methods.ts +221 -0
- package/backend/publication-job.ts +625 -0
- package/backend/push.ts +287 -0
- package/backend/schedule-methods.ts +120 -0
- package/backend/share.ts +221 -0
- package/backend/tool-family.ts +124 -0
- package/backend/tsconfig.build.json +11 -0
- package/backend/tsconfig.json +15 -0
- package/dist/backend/catalog.js +42 -0
- package/dist/backend/gills.js +49 -0
- package/dist/backend/indexes.js +294 -0
- package/dist/backend/library-methods.js +219 -0
- package/dist/backend/library.js +454 -0
- package/dist/backend/methods.js +141 -0
- package/dist/backend/publication-job.js +451 -0
- package/dist/backend/schedule-methods.js +90 -0
- package/dist/backend/share.js +140 -0
- package/dist/backend/tool-family.js +98 -0
- package/dist/shared/tool-contracts.js +32 -0
- package/package.json +166 -3
- package/shared/tool-contracts.ts +35 -0
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
|
-
#
|
|
1
|
+
# Trellis (`@fungi.computer/trellis`)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+

|
|
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
|
+
}
|
package/backend/gills.ts
ADDED
|
@@ -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
|
+
});
|
package/backend/index.ts
ADDED
|
@@ -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);
|