trellis 3.0.3 → 3.1.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 3.1.2
4
+
5
+ - Fixed `trellis/cms` collection reads for inferred collections whose normalized key differs from the stored entity type casing, such as `blogpost` reading `BlogPost` entities.
6
+ - Added graph-link awareness to CMS entries so reference links such as `post --author--> author` appear in `fields` and can be expanded.
7
+ - Added `status` fact fallback when `cms_status` is absent, preserving content created by agents or lower-level store tools.
8
+ - Added shared polling for CMS subscriptions so duplicate subscribers to the same collection or entry reuse one poll stream.
9
+ - Added `onError` and custom `equals` subscription options.
10
+ - Fixed CMS entity pagination to respect the opencode store route's 1000-entity page limit.
11
+ - Added CMS client/scaffold tests and included them in the default test script.
12
+ - Added `directory` support to CMS consumer scaffolds for multi-instance opencode routing.
13
+
14
+ ## 3.1.1
15
+
16
+ - Published the first `trellis/cms` SDK package update after adding scaffold helpers.
17
+
18
+ ## 3.1.0
19
+
20
+ - Added the `trellis/cms` subpath with `createCmsClient`, collection reads, entry reads, polling subscriptions, reference expansion, collection discovery, and consumer scaffold helpers for vanilla, React, Solid, and Vue.
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Trellis
2
2
 
3
- https://trellis.computer
4
-
5
- > **The Agentic Framework** — A structured runtime for building agents that understand code, remember everything, and explain themselves.
3
+ > **The Agentic Framework**
6
4
 
7
5
  | Use Case | How |
8
6
  | :----------------------------- | :-------------------------------------------------------------------------------------------------------- |
@@ -46,10 +44,49 @@ trellis milestone "Initial Release"
46
44
  trellis code
47
45
  ```
48
46
 
47
+ ![Trellis Demo](./trellis-opencode.gif)
48
+
49
49
  See the [CLI guide](https://trellis.computer/docs/cli) for complete documentation.
50
50
 
51
51
  ---
52
52
 
53
+ ## CMS Client SDK
54
+
55
+ Use `trellis/cms` from browser apps to read content from a Trellis-compatible HTTP server such as opencode at `http://localhost:4096`.
56
+
57
+ ```typescript
58
+ import { createCmsClient } from 'trellis/cms';
59
+
60
+ const cms = createCmsClient({
61
+ url: 'http://localhost:4096',
62
+ directory: '/path/to/project',
63
+ });
64
+
65
+ const posts = await cms.collection('blogpost').list({
66
+ status: 'all',
67
+ expand: ['author'],
68
+ });
69
+
70
+ const off = cms.collection('blogpost').subscribe(
71
+ (entries) => {
72
+ console.log(entries);
73
+ },
74
+ {
75
+ status: 'published',
76
+ onError: console.error,
77
+ },
78
+ );
79
+
80
+ off();
81
+ ```
82
+
83
+ - **Collections**: `cms.collections()` returns explicit `TypeSchema` collections and inferred collections from existing entity types.
84
+ - **Normalized keys**: `cms.collection('blogpost')` matches stored entity types like `BlogPost`.
85
+ - **References**: `expand` resolves reference ids from facts and graph links into nested entries.
86
+ - **Scaffolds**: `scaffoldConsumer({ collection, framework, directory })` generates starter consumer code for vanilla, React, Solid, or Vue.
87
+
88
+ ---
89
+
53
90
  ## What is Trellis?
54
91
 
55
92
  Trellis is an **event-sourced causal graph engine** that unifies version control, knowledge management, and semantic analysis. Every action is an immutable operation in a causal stream:
@@ -80,7 +117,11 @@ Ops are written to `.trellis/ops.json` and **never rewritten or deleted**.
80
117
 
81
118
  ## Documentation
82
119
 
83
- - **[Full documentation](https://trellis.computer)** — Complete guides and API reference
120
+ - **[Local documentation](./docs/README.md)** — Canonical docs for the active codebase
121
+ - **[Vision](./docs/VISION.md)** — Local-first agentic OS framing
122
+ - **[Architecture](./docs/ARCHITECTURE.md)** — Current package layout and target runtime shape
123
+ - **[Roadmap](./ROADMAP.md)** — Active Trellis issue and milestone sequence
124
+ - **[Full documentation](https://trellis.computer)** — Published docs site
84
125
  - **[CLI reference](./README-ARCHIVED.md#cli-overview)** — Command details (archived)
85
126
  - **[API modules](./README-ARCHIVED.md#module--subpath-guide)** — Subpath imports (archived)
86
127
  - **[DESIGN.md](./DESIGN.md)** — Architecture specification
package/dist/cli/index.js CHANGED
@@ -2886,7 +2886,7 @@ var {
2886
2886
  Help
2887
2887
  } = import__.default;
2888
2888
 
2889
- // ../trellis-client/node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
2889
+ // node_modules/chalk/source/vendor/ansi-styles/index.js
2890
2890
  var ANSI_BACKGROUND_OFFSET = 10;
2891
2891
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
2892
2892
  var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
@@ -3063,7 +3063,7 @@ function assembleStyles() {
3063
3063
  var ansiStyles = assembleStyles();
3064
3064
  var ansi_styles_default = ansiStyles;
3065
3065
 
3066
- // ../trellis-client/node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
3066
+ // node_modules/chalk/source/vendor/supports-color/index.js
3067
3067
  import process2 from "process";
3068
3068
  import os from "os";
3069
3069
  import tty from "tty";
@@ -3195,7 +3195,7 @@ var supportsColor = {
3195
3195
  };
3196
3196
  var supports_color_default = supportsColor;
3197
3197
 
3198
- // ../trellis-client/node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/utilities.js
3198
+ // node_modules/chalk/source/utilities.js
3199
3199
  function stringReplaceAll(string, substring, replacer) {
3200
3200
  let index = string.indexOf(substring);
3201
3201
  if (index === -1) {
@@ -3228,7 +3228,7 @@ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
3228
3228
  return returnValue;
3229
3229
  }
3230
3230
 
3231
- // ../trellis-client/node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/index.js
3231
+ // node_modules/chalk/source/index.js
3232
3232
  var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
3233
3233
  var GENERATOR = Symbol("GENERATOR");
3234
3234
  var STYLER = Symbol("STYLER");
@@ -5716,16 +5716,13 @@ function requireRepo(rootPath) {
5716
5716
  process.exit(1);
5717
5717
  }
5718
5718
  }
5719
- program2.command("init").description("Initialize a new TrellisVCS repository in the current directory").option("-p, --path <path>", "Path to initialize", ".").option("--ides <ides...>", "IDEs to scaffold for (cursor, windsurf, claude, copilot, codex, gemini)").option("--framework <framework>", "Project framework (react, vue, svelte, next, nuxt, remotion, expo, bun, node, cli, library, animation, games, none)", "none").option("--footprint <footprint>", "Workspace footprint (minimal, standard, full)", "standard").option("--no-interactive", "Skip interactive prompts").action(async (opts) => {
5720
- const rootPath = resolve(opts.path);
5721
- const isInteractive = process.stdout.isTTY && !process.argv.includes("--no-interactive");
5719
+ async function runInit(rootPath, opts = {}) {
5720
+ const isInteractive = opts.interactive !== false && process.stdout.isTTY && !process.argv.includes("--no-interactive");
5722
5721
  if (TrellisVcsEngine.isRepo(rootPath)) {
5723
- console.log(source_default.yellow("Already a Trellis workspace."));
5724
5722
  return;
5725
5723
  }
5726
5724
  if (!hasProfile()) {
5727
- const trellisDir = join6(rootPath, ".trellis");
5728
- const identity = hasIdentity(trellisDir) ? loadIdentity(trellisDir) : null;
5725
+ const identity = hasIdentity(rootPath) ? loadIdentity(join6(rootPath, ".trellis")) : null;
5729
5726
  if (isInteractive) {
5730
5727
  console.log(source_default.cyan(`
5731
5728
  Welcome to Trellis! Let's get you set up.`));
@@ -5884,7 +5881,6 @@ program2.command("init").description("Initialize a new TrellisVCS repository in
5884
5881
  }
5885
5882
  if (selectedIdes && selectedIdes.length > 0) {
5886
5883
  for (const selectedIde of selectedIdes) {
5887
- console.log(source_default.dim(` Scaffolding ${selectedIde} dotfolder...`));
5888
5884
  writeIdeScaffold(rootPath, {
5889
5885
  ide: selectedIde,
5890
5886
  footprint,
@@ -5896,25 +5892,34 @@ program2.command("init").description("Initialize a new TrellisVCS repository in
5896
5892
  });
5897
5893
  }
5898
5894
  }
5895
+ }
5896
+ program2.command("init").description("Initialize a new TrellisVCS repository in the current directory").option("-p, --path <path>", "Path to initialize", ".").option("--ides <ides...>", "IDEs to scaffold for (cursor, windsurf, claude, copilot, codex, gemini)").option("--framework <framework>", "Project framework (react, vue, svelte, next, nuxt, remotion, expo, bun, node, cli, library, animation, games, none)", "none").option("--footprint <footprint>", "Workspace footprint (minimal, standard, full)", "standard").option("--no-interactive", "Skip interactive prompts").action(async (opts) => {
5897
+ const rootPath = resolve(opts.path);
5898
+ if (TrellisVcsEngine.isRepo(rootPath)) {
5899
+ console.log(source_default.yellow("Already a Trellis workspace."));
5900
+ return;
5901
+ }
5902
+ await runInit(rootPath, {
5903
+ interactive: process.stdout.isTTY && !process.argv.includes("--no-interactive"),
5904
+ ides: opts.ides,
5905
+ framework: opts.framework,
5906
+ footprint: opts.footprint
5907
+ });
5899
5908
  console.log(source_default.green("\u2713 Initialized Trellis repository"));
5909
+ const engine = new TrellisVcsEngine({ rootPath });
5910
+ const opsCount = (await engine.log()).length;
5900
5911
  console.log(` ${source_default.dim("Path:")} ${rootPath}`);
5901
- console.log(` ${source_default.dim("Ops:")} ${result.opsCreated} initial operations scanned`);
5912
+ console.log(` ${source_default.dim("Ops:")} ${opsCount} initial operations scanned`);
5902
5913
  console.log(` ${source_default.dim("Config:")} .trellis/config.json`);
5903
5914
  console.log(` ${source_default.dim("Op log:")} .trellis/ops.json`);
5904
5915
  console.log(` ${source_default.dim("Graph DB:")} .trellis/graph.db`);
5905
5916
  console.log(` ${source_default.dim("Agent context:")} .trellis/agents/AGENTS.md`);
5906
- if (selectedIdes.length > 0) {
5907
- const ideFolders = selectedIdes.map((id) => {
5908
- const ide = id;
5909
- return ide === "cursor" ? ".cursor" : ide === "windsurf" ? ".windsurf" : ide === "claude" ? ".claude" : ide === "codex" ? ".codex" : ide === "gemini" ? ".gemini" : ".github/copilot";
5910
- });
5911
- console.log(` ${source_default.dim("IDE rules:")} ${ideFolders.join(", ")}`);
5912
- }
5913
- if (result.context.domain) {
5914
- console.log(` ${source_default.dim("Domain:")} ${source_default.cyan(result.context.domain)} ${source_default.dim("(inferred)")}`);
5917
+ const preInfer = await inferProjectContext(rootPath);
5918
+ if (preInfer.domain) {
5919
+ console.log(` ${source_default.dim("Domain:")} ${source_default.cyan(preInfer.domain)} ${source_default.dim("(inferred)")}`);
5915
5920
  }
5916
- if (result.context.ecosystem && result.context.ecosystem !== "unknown") {
5917
- console.log(` ${source_default.dim("Ecosystem:")} ${result.context.ecosystem}`);
5921
+ if (preInfer.ecosystem && preInfer.ecosystem !== "unknown") {
5922
+ console.log(` ${source_default.dim("Ecosystem:")} ${preInfer.ecosystem}`);
5918
5923
  }
5919
5924
  console.log();
5920
5925
  console.log(source_default.bold("Next steps:"));
@@ -5925,7 +5930,7 @@ program2.command("init").description("Initialize a new TrellisVCS repository in
5925
5930
  console.log(` ${source_default.cyan("trellis milestone")} Create narrative checkpoints`);
5926
5931
  console.log(` ${source_default.cyan("trellis garden")} Discover abandoned work`);
5927
5932
  console.log(` ${source_default.cyan("trellis issue")} Create and track issues`);
5928
- if (result.context.confidence !== "high") {
5933
+ if (preInfer.confidence !== "high") {
5929
5934
  console.log(` ${source_default.cyan("trellis season")} Enrich project context for agents`);
5930
5935
  }
5931
5936
  console.log();
@@ -8573,16 +8578,21 @@ program2.command("season").description("Enrich project context for agents \u2014
8573
8578
  }
8574
8579
  console.log();
8575
8580
  });
8576
- program2.command("code").alias("ide").description('Launch OpenCode in "Harness" mode, bridged to this Trellis repository').option("-p, --path <path>", "Repository path", ".").option("-m, --model <model>", "OpenCode model to use").option("-w, --web", "Launch MCP server in HTTP mode for web client access").option("--mcp-port <port>", "MCP HTTP server port (default: 3333)", "3333").action(async (opts) => {
8581
+ program2.command("code").alias("ide").description('Launch OpenCode in "Harness" mode, bridged to this Trellis repository').option("-p, --path <path>", "Repository path", ".").option("-m, --model <model>", "OpenCode model to use").option("-w, --web", "Launch MCP server in HTTP mode for web client access").option("--mcp-port <port>", "MCP HTTP server port (default: 3333)", "3333").option("--no-init", "Skip initialization even if not a Trellis workspace").action(async (opts) => {
8577
8582
  const { resolve: resolve2, dirname: dirname6, join: join7 } = await import("path");
8578
8583
  const { existsSync: existsSync6 } = await import("fs");
8579
8584
  const { spawn } = await import("child_process");
8585
+ const { readFileSync: readFileSync4 } = await import("fs");
8586
+ const { createServer: createHttpServer } = await import("http");
8580
8587
  const rootPath = resolve2(opts.path);
8581
- if (!TrellisVcsEngine.isRepo(rootPath)) {
8582
- console.log(source_default.dim("Not a Trellis workspace \u2014 initializing\u2026"));
8583
- const engine = new TrellisVcsEngine({ rootPath });
8584
- await engine.initRepo();
8585
- console.log(source_default.green("\u2713 Initialized Trellis repository"));
8588
+ if (!opts.noInit) {
8589
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
8590
+ console.log(source_default.dim("Not a Trellis workspace \u2014 initializing\u2026"));
8591
+ await runInit(rootPath, {
8592
+ interactive: false
8593
+ });
8594
+ console.log(source_default.green("\u2713 Initialized Trellis repository"));
8595
+ }
8586
8596
  }
8587
8597
  function findOpencode() {
8588
8598
  const here = dirname6(process.argv[1]);
@@ -8639,24 +8649,46 @@ program2.command("code").alias("ide").description('Launch OpenCode in "Harness"
8639
8649
  console.log(source_default.dim(` Web: ${mcpUrl}`));
8640
8650
  }
8641
8651
  console.log("");
8652
+ let mcpProcess = null;
8653
+ if (opts.web) {
8654
+ console.log(source_default.dim(` Starting MCP server on port ${mcpPort}\u2026`));
8655
+ mcpProcess = spawn("bun", ["run", mcp, "--quiet", "--path", rootPath, "--http", "--port", String(mcpPort)], {
8656
+ stdio: ["pipe", "pipe", "pipe"],
8657
+ detached: false
8658
+ });
8659
+ mcpProcess.stderr?.on("data", (data) => {
8660
+ const msg = data.toString();
8661
+ if (!msg.includes("TrellisVCS MCP Server")) {
8662
+ process.stderr.write(source_default.dim(` [mcp] ${msg}`));
8663
+ }
8664
+ });
8665
+ mcpProcess.on("error", (err) => {
8666
+ console.error(source_default.red(`
8667
+ \u2717 MCP server failed to start: ${err.message}`));
8668
+ process.exit(1);
8669
+ });
8670
+ const ready = await waitForMcpReady(mcpUrl, 1e4);
8671
+ if (!ready) {
8672
+ console.error(source_default.red(`
8673
+ \u2717 MCP server did not become ready in time`));
8674
+ if (mcpProcess) {
8675
+ mcpProcess.kill();
8676
+ }
8677
+ process.exit(1);
8678
+ }
8679
+ console.log(source_default.green(`\u2713 MCP server ready at ${mcpUrl}`));
8680
+ }
8642
8681
  const args = [rootPath];
8643
8682
  if (opts.model) {
8644
8683
  args.push("--model", opts.model);
8645
8684
  }
8646
8685
  const mcpServers = {
8647
8686
  mcpServers: {
8648
- "trellis-vcs": {
8687
+ "trellis-vcs": opts.web ? {
8688
+ url: `${mcpUrl}/sse`
8689
+ } : {
8649
8690
  command: "bun",
8650
- args: opts.web ? [
8651
- "run",
8652
- mcp,
8653
- "--quiet",
8654
- "--path",
8655
- rootPath,
8656
- "--http",
8657
- "--port",
8658
- String(mcpPort)
8659
- ] : ["run", mcp, "--quiet", "--path", rootPath]
8691
+ args: ["run", mcp, "--quiet", "--path", rootPath]
8660
8692
  }
8661
8693
  }
8662
8694
  };
@@ -8672,6 +8704,9 @@ program2.command("code").alias("ide").description('Launch OpenCode in "Harness"
8672
8704
  }
8673
8705
  });
8674
8706
  child.on("exit", (code) => {
8707
+ if (mcpProcess) {
8708
+ mcpProcess.kill();
8709
+ }
8675
8710
  if (code === 0) {
8676
8711
  console.log(source_default.green(`
8677
8712
  \u2713 Agentic session complete.`));
@@ -8682,4 +8717,22 @@ program2.command("code").alias("ide").description('Launch OpenCode in "Harness"
8682
8717
  process.exit(code ?? 0);
8683
8718
  });
8684
8719
  });
8720
+ async function waitForMcpReady(url, timeoutMs) {
8721
+ const { get } = await import("http");
8722
+ const start = Date.now();
8723
+ while (Date.now() - start < timeoutMs) {
8724
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
8725
+ try {
8726
+ const res = await new Promise((resolve2, reject) => {
8727
+ const req = get(`${url}/health`, resolve2);
8728
+ req.on("error", reject);
8729
+ req.setTimeout(2000);
8730
+ });
8731
+ if (res.statusCode === 200) {
8732
+ return true;
8733
+ }
8734
+ } catch {}
8735
+ }
8736
+ return false;
8737
+ }
8685
8738
  program2.parse();
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Trellis CMS Client
3
+ *
4
+ * Thin HTTP client for reading content collections from a Trellis-compatible store
5
+ * (currently opencode's /store/* routes). Reads only; writes happen through the
6
+ * IDE's CMS panel or the agent's `cms` tool.
7
+ *
8
+ * @example
9
+ * import { createCmsClient } from "trellis/cms";
10
+ *
11
+ * const cms = createCmsClient({ url: "http://localhost:4096" });
12
+ *
13
+ * // List published blog posts with author resolved
14
+ * const posts = await cms.collection("blog_post").list({
15
+ * status: "published",
16
+ * expand: ["author"],
17
+ * });
18
+ *
19
+ * // Subscribe to live updates (polling for now, SSE later)
20
+ * const off = cms.collection("blog_post").subscribe((entries) => {
21
+ * console.log(entries);
22
+ * });
23
+ *
24
+ * @module trellis/cms
25
+ */
26
+ import type { CmsClientOptions, Collection, Entry, EntrySubscribeOptions, EntrySubscriber, GetOptions, ListOptions, ListSubscribeOptions, ListSubscriber, Unsubscribe } from './types.js';
27
+ import { type RawEntity } from './internal.js';
28
+ export declare class CmsClient {
29
+ private readonly url;
30
+ private readonly basePath;
31
+ private readonly directory?;
32
+ readonly pollIntervalMs: number;
33
+ private readonly fetchFn;
34
+ private readonly apiKey?;
35
+ private readonly subscriptions;
36
+ constructor(opts: CmsClientOptions);
37
+ collection<T extends Record<string, unknown> = Record<string, unknown>>(key: string): CollectionRef<T>;
38
+ entry<T extends Record<string, unknown> = Record<string, unknown>>(id: string): EntryRef<T>;
39
+ /** List all CMS collections (explicit + inferred). */
40
+ collections(): Promise<Collection[]>;
41
+ close(): void;
42
+ /**
43
+ * Shared polling subscription. Multiple subscribers to the same key share a
44
+ * single timer and one HTTP request per poll cycle. New subscribers receive
45
+ * the most recently fetched value immediately if one is cached.
46
+ *
47
+ * @internal
48
+ */
49
+ _share<T>(key: string, fetcher: () => Promise<T>, callback: (value: T) => void, extras?: {
50
+ equals?: (prev: unknown, next: unknown) => boolean;
51
+ onError?: (err: unknown) => void;
52
+ }): Unsubscribe;
53
+ /** @internal */
54
+ _get<T>(path: string): Promise<T | undefined>;
55
+ /** @internal */
56
+ _entities(): Promise<RawEntity[]>;
57
+ /** @internal */
58
+ _entryById(id: string): Promise<Entry | null>;
59
+ }
60
+ export declare class CollectionRef<T extends Record<string, unknown> = Record<string, unknown>> {
61
+ private readonly client;
62
+ readonly key: string;
63
+ constructor(client: CmsClient, key: string);
64
+ list(opts?: ListOptions): Promise<Entry<T>[]>;
65
+ get(id: string, opts?: GetOptions): Promise<Entry<T> | null>;
66
+ /**
67
+ * Subscribe to changes. Currently implemented as polling; a future SSE-backed
68
+ * upgrade will replace the transport without changing this API.
69
+ *
70
+ * Multiple subscribers to the same collection + opts share one polling timer
71
+ * and one HTTP request per cycle.
72
+ */
73
+ subscribe(callback: ListSubscriber<T>, opts?: ListSubscribeOptions): Unsubscribe;
74
+ }
75
+ export declare class EntryRef<T extends Record<string, unknown> = Record<string, unknown>> {
76
+ private readonly client;
77
+ readonly id: string;
78
+ constructor(client: CmsClient, id: string);
79
+ get(opts?: GetOptions): Promise<Entry<T> | null>;
80
+ subscribe(callback: EntrySubscriber<T>, opts?: EntrySubscribeOptions): Unsubscribe;
81
+ }
82
+ export declare function createCmsClient(opts: CmsClientOptions): CmsClient;
83
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/cms/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Trellis CMS — client-side SDK for reading content collections.
3
+ *
4
+ * @module trellis/cms
5
+ */
6
+ export { CmsClient, CollectionRef, EntryRef, createCmsClient } from './client.js';
7
+ export type { CmsClientOptions, Collection, Entry, EntryStatus, EntrySubscribeOptions, EntrySubscriber, Framework, GetOptions, ListOptions, ListSubscribeOptions, ListSubscriber, SubscribeExtras, Unsubscribe, } from './types.js';
8
+ export { scaffoldConsumer, scaffoldFilename } from './scaffold.js';
9
+ export type { ScaffoldOptions } from './scaffold.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cms/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAElF,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,KAAK,EACL,WAAW,EACX,qBAAqB,EACrB,eAAe,EACf,SAAS,EACT,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,492 @@
1
+ // @bun
2
+ import"../index-a76rekgs.js";
3
+
4
+ // src/cms/internal.ts
5
+ var SYSTEM_TYPES = new Set([
6
+ "issue",
7
+ "agent",
8
+ "project",
9
+ "memory",
10
+ "mcp",
11
+ "sprite",
12
+ "workunit",
13
+ "cycle",
14
+ "epic",
15
+ "roadmap",
16
+ "suggestion",
17
+ "file",
18
+ "directory",
19
+ "op",
20
+ "branch",
21
+ "decision",
22
+ "session",
23
+ "typeschema",
24
+ "field"
25
+ ]);
26
+ function isSystemType(type) {
27
+ return SYSTEM_TYPES.has(type.trim().toLowerCase());
28
+ }
29
+ function typeKey(type) {
30
+ return type.trim().toLowerCase();
31
+ }
32
+ function entryFromFacts(entity, facts, links) {
33
+ const fields = {};
34
+ let status = "draft";
35
+ let cmsStatusSeen = false;
36
+ for (const f of facts) {
37
+ if (f.a === "type")
38
+ continue;
39
+ if (f.a === "cms_status") {
40
+ status = f.v === "published" ? "published" : "draft";
41
+ cmsStatusSeen = true;
42
+ continue;
43
+ }
44
+ fields[f.a] = f.v;
45
+ }
46
+ if (!cmsStatusSeen) {
47
+ if (fields.status === "published")
48
+ status = "published";
49
+ else if (fields.status === "draft")
50
+ status = "draft";
51
+ }
52
+ if (links) {
53
+ for (const link of links) {
54
+ if (link.e1 !== entity.id)
55
+ continue;
56
+ if (!(link.a in fields))
57
+ fields[link.a] = link.e2;
58
+ }
59
+ }
60
+ return { id: entity.id, type: entity.type, status, fields };
61
+ }
62
+ function groupFactsByEntity(facts) {
63
+ const map = new Map;
64
+ for (const f of facts) {
65
+ const list = map.get(f.e);
66
+ if (list)
67
+ list.push(f);
68
+ else
69
+ map.set(f.e, [f]);
70
+ }
71
+ return map;
72
+ }
73
+ function groupLinksBySource(links) {
74
+ const map = new Map;
75
+ for (const l of links) {
76
+ const list = map.get(l.e1);
77
+ if (list)
78
+ list.push(l);
79
+ else
80
+ map.set(l.e1, [l]);
81
+ }
82
+ return map;
83
+ }
84
+ async function expandReferences(entries, expandKeys, fetchEntity) {
85
+ const ids = new Set;
86
+ for (const entry of entries) {
87
+ for (const key of expandKeys) {
88
+ const v = entry.fields[key];
89
+ if (typeof v === "string")
90
+ ids.add(v);
91
+ }
92
+ }
93
+ if (ids.size === 0)
94
+ return entries;
95
+ const resolved = new Map;
96
+ await Promise.all([...ids].map(async (id) => {
97
+ try {
98
+ resolved.set(id, await fetchEntity(id));
99
+ } catch {
100
+ resolved.set(id, null);
101
+ }
102
+ }));
103
+ return entries.map((entry) => {
104
+ const next = { ...entry.fields };
105
+ for (const key of expandKeys) {
106
+ const v = next[key];
107
+ if (typeof v === "string" && resolved.has(v)) {
108
+ next[key] = resolved.get(v);
109
+ }
110
+ }
111
+ return { ...entry, fields: next };
112
+ });
113
+ }
114
+ function fingerprint(value) {
115
+ return JSON.stringify(value);
116
+ }
117
+
118
+ // src/cms/client.ts
119
+ var DEFAULT_BASE_PATH = "/trellis/store";
120
+ var DEFAULT_POLL_MS = 2000;
121
+ var MIN_POLL_MS = 500;
122
+ var MAX_FACTS_PER_FETCH = 5000;
123
+ var MAX_ENTITIES_PER_FETCH = 1000;
124
+ var defaultEquals = (prev, next) => fingerprint(prev) === fingerprint(next);
125
+
126
+ class CmsClient {
127
+ url;
128
+ basePath;
129
+ directory;
130
+ pollIntervalMs;
131
+ fetchFn;
132
+ apiKey;
133
+ subscriptions = new Map;
134
+ constructor(opts) {
135
+ this.url = opts.url.replace(/\/+$/, "");
136
+ this.basePath = (opts.basePath ?? DEFAULT_BASE_PATH).replace(/\/+$/, "");
137
+ this.directory = opts.directory;
138
+ this.pollIntervalMs = Math.max(MIN_POLL_MS, opts.pollIntervalMs ?? DEFAULT_POLL_MS);
139
+ this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
140
+ this.apiKey = opts.apiKey;
141
+ }
142
+ collection(key) {
143
+ return new CollectionRef(this, key);
144
+ }
145
+ entry(id) {
146
+ return new EntryRef(this, id);
147
+ }
148
+ async collections() {
149
+ const [entities, facts] = await Promise.all([
150
+ this._entities(),
151
+ this._get(`/facts?limit=${MAX_FACTS_PER_FETCH}`)
152
+ ]);
153
+ const factsByEntity = groupFactsByEntity(facts ?? []);
154
+ const counts = new Map;
155
+ const canonical = new Map;
156
+ for (const e of entities ?? []) {
157
+ const k = typeKey(e.type);
158
+ counts.set(k, (counts.get(k) ?? 0) + 1);
159
+ if (!canonical.has(k))
160
+ canonical.set(k, e.type);
161
+ }
162
+ const out = new Map;
163
+ for (const e of entities ?? []) {
164
+ if (e.type !== "TypeSchema")
165
+ continue;
166
+ const efacts = factsByEntity.get(e.id) ?? [];
167
+ const isCms = efacts.some((f) => f.a === "cms" && f.v === true);
168
+ if (!isCms)
169
+ continue;
170
+ const name = e.id.replace(/^schema:/, "");
171
+ const k = typeKey(name);
172
+ const labelFact = efacts.find((f) => f.a === "label");
173
+ const label = typeof labelFact?.v === "string" ? labelFact.v : name;
174
+ out.set(k, {
175
+ key: k,
176
+ label,
177
+ inferred: false,
178
+ count: counts.get(k) ?? 0
179
+ });
180
+ }
181
+ for (const [k, count] of counts) {
182
+ if (isSystemType(k) || out.has(k))
183
+ continue;
184
+ out.set(k, {
185
+ key: k,
186
+ label: canonical.get(k) ?? k,
187
+ inferred: true,
188
+ count
189
+ });
190
+ }
191
+ return [...out.values()].sort((a, b) => a.label.localeCompare(b.label));
192
+ }
193
+ close() {
194
+ for (const sub of this.subscriptions.values()) {
195
+ clearInterval(sub.interval);
196
+ sub.subscribers.clear();
197
+ }
198
+ this.subscriptions.clear();
199
+ }
200
+ _share(key, fetcher, callback, extras = {}) {
201
+ let sub = this.subscriptions.get(key);
202
+ if (!sub) {
203
+ const fresh = {
204
+ subscribers: new Set,
205
+ interval: undefined,
206
+ hasLast: false,
207
+ fetcher
208
+ };
209
+ const tick = async () => {
210
+ try {
211
+ const next = await fresh.fetcher();
212
+ fresh.last = next;
213
+ fresh.hasLast = true;
214
+ for (const item2 of fresh.subscribers) {
215
+ if (!item2.hasLast || !item2.equals(item2.last, next)) {
216
+ item2.last = next;
217
+ item2.hasLast = true;
218
+ item2.callback(next);
219
+ }
220
+ }
221
+ } catch (err) {
222
+ for (const item2 of fresh.subscribers)
223
+ item2.onError?.(err);
224
+ }
225
+ };
226
+ fresh.interval = setInterval(tick, this.pollIntervalMs);
227
+ this.subscriptions.set(key, fresh);
228
+ sub = fresh;
229
+ tick();
230
+ }
231
+ const item = {
232
+ callback,
233
+ equals: extras.equals ?? defaultEquals,
234
+ onError: extras.onError,
235
+ hasLast: false
236
+ };
237
+ sub.subscribers.add(item);
238
+ if (sub.hasLast) {
239
+ item.last = sub.last;
240
+ item.hasLast = true;
241
+ callback(sub.last);
242
+ }
243
+ return () => {
244
+ sub.subscribers.delete(item);
245
+ if (sub.subscribers.size === 0) {
246
+ clearInterval(sub.interval);
247
+ this.subscriptions.delete(key);
248
+ }
249
+ };
250
+ }
251
+ async _get(path) {
252
+ const u = new URL(`${this.basePath}${path}`, this.url);
253
+ if (this.directory && !u.searchParams.has("directory")) {
254
+ u.searchParams.set("directory", this.directory);
255
+ }
256
+ const res = await this.fetchFn(u.toString(), {
257
+ headers: this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
258
+ });
259
+ if (res.status === 404)
260
+ return;
261
+ if (!res.ok)
262
+ throw new Error(`Trellis CMS request failed (${res.status}) ${u.pathname}`);
263
+ return await res.json();
264
+ }
265
+ async _entities() {
266
+ const out = [];
267
+ let offset = 0;
268
+ while (true) {
269
+ const page = await this._get(`/entities?limit=${MAX_ENTITIES_PER_FETCH}&offset=${offset}`) ?? [];
270
+ out.push(...page);
271
+ if (page.length < MAX_ENTITIES_PER_FETCH)
272
+ return out;
273
+ offset += MAX_ENTITIES_PER_FETCH;
274
+ }
275
+ }
276
+ async _entryById(id) {
277
+ const detail = await this._get(`/entity/${encodeURIComponent(id)}`);
278
+ if (!detail)
279
+ return null;
280
+ const typeFact = detail.facts.find((f) => f.a === "type");
281
+ const type = typeof typeFact?.v === "string" ? typeFact.v : "unknown";
282
+ return entryFromFacts({ id: detail.id, type }, detail.facts, detail.links);
283
+ }
284
+ }
285
+
286
+ class CollectionRef {
287
+ client;
288
+ key;
289
+ constructor(client, key) {
290
+ this.client = client;
291
+ this.key = key;
292
+ }
293
+ async list(opts = {}) {
294
+ const status = opts.status ?? "published";
295
+ const limit = opts.limit ?? 100;
296
+ const wantsExpand = opts.expand && opts.expand.length > 0;
297
+ const [allEntities, facts, links] = await Promise.all([
298
+ this.client._entities(),
299
+ this.client._get(`/facts?limit=${MAX_FACTS_PER_FETCH}`),
300
+ this.client._get(`/links`)
301
+ ]);
302
+ if (!allEntities)
303
+ return [];
304
+ const wantedKey = typeKey(this.key);
305
+ const matching = allEntities.filter((e) => typeKey(e.type) === wantedKey);
306
+ if (matching.length === 0)
307
+ return [];
308
+ const factsByEntity = groupFactsByEntity(facts ?? []);
309
+ const linksBySource = groupLinksBySource(links ?? []);
310
+ let entries = matching.map((e) => entryFromFacts(e, factsByEntity.get(e.id) ?? [], linksBySource.get(e.id) ?? []));
311
+ if (status !== "all") {
312
+ entries = entries.filter((e) => e.status === status);
313
+ }
314
+ entries = entries.slice(0, limit);
315
+ if (wantsExpand) {
316
+ entries = await expandReferences(entries, opts.expand, (id) => this.client._entryById(id));
317
+ }
318
+ return entries;
319
+ }
320
+ async get(id, opts = {}) {
321
+ const entry = await this.client._entryById(id);
322
+ if (!entry)
323
+ return null;
324
+ if (opts.expand && opts.expand.length > 0) {
325
+ const [expanded] = await expandReferences([entry], opts.expand, (eid) => this.client._entryById(eid));
326
+ return expanded;
327
+ }
328
+ return entry;
329
+ }
330
+ subscribe(callback, opts = {}) {
331
+ const { onError, equals, ...listOpts } = opts;
332
+ const key = `coll:${typeKey(this.key)}:${JSON.stringify(listOpts)}`;
333
+ return this.client._share(key, () => this.list(listOpts), callback, { onError, equals });
334
+ }
335
+ }
336
+
337
+ class EntryRef {
338
+ client;
339
+ id;
340
+ constructor(client, id) {
341
+ this.client = client;
342
+ this.id = id;
343
+ }
344
+ async get(opts = {}) {
345
+ const entry = await this.client._entryById(this.id);
346
+ if (!entry)
347
+ return null;
348
+ if (opts.expand && opts.expand.length > 0) {
349
+ const [expanded] = await expandReferences([entry], opts.expand, (eid) => this.client._entryById(eid));
350
+ return expanded;
351
+ }
352
+ return entry;
353
+ }
354
+ subscribe(callback, opts = {}) {
355
+ const { onError, equals, ...getOpts } = opts;
356
+ const key = `entry:${this.id}:${JSON.stringify(getOpts)}`;
357
+ return this.client._share(key, () => this.get(getOpts), callback, { onError, equals });
358
+ }
359
+ }
360
+ function createCmsClient(opts) {
361
+ return new CmsClient(opts);
362
+ }
363
+ // src/cms/scaffold.ts
364
+ var DEFAULT_URL = "http://localhost:4096";
365
+ function expandLiteral(expand) {
366
+ if (!expand || expand.length === 0)
367
+ return "";
368
+ return ` expand: ${JSON.stringify(expand)},`;
369
+ }
370
+ function clientLiteral(url, directory) {
371
+ if (!directory)
372
+ return `createCmsClient({ url: "${url}" })`;
373
+ return `createCmsClient({ url: "${url}", directory: ${JSON.stringify(directory)} })`;
374
+ }
375
+ function vanilla(opts) {
376
+ const url = opts.url ?? DEFAULT_URL;
377
+ const exp = expandLiteral(opts.expand);
378
+ return `import { createCmsClient } from "trellis/cms";
379
+
380
+ const cms = ${clientLiteral(url, opts.directory)};
381
+
382
+ const collection = cms.collection("${opts.collection}");
383
+
384
+ // One-shot fetch (defaults to status: "published")
385
+ const entries = await collection.list({${exp}});
386
+ console.log(entries);
387
+
388
+ // Live updates \u2014 re-fires whenever the collection changes
389
+ const off = collection.subscribe(
390
+ (entries) => {
391
+ console.log("Updated:", entries);
392
+ },
393
+ {${exp}},
394
+ );
395
+ // off(); // call to stop receiving updates
396
+ `;
397
+ }
398
+ function react(opts) {
399
+ const url = opts.url ?? DEFAULT_URL;
400
+ const exp = expandLiteral(opts.expand);
401
+ return `import { useEffect, useState } from "react";
402
+ import { createCmsClient, type Entry } from "trellis/cms";
403
+
404
+ const cms = ${clientLiteral(url, opts.directory)};
405
+
406
+ export function use${pascal(opts.collection)}() {
407
+ const [entries, setEntries] = useState<Entry[]>([]);
408
+ useEffect(() => {
409
+ const off = cms.collection("${opts.collection}").subscribe(setEntries, {${exp}});
410
+ return off;
411
+ }, []);
412
+ return entries;
413
+ }
414
+
415
+ // Usage:
416
+ // const posts = use${pascal(opts.collection)}();
417
+ // return posts.map(p => <article key={p.id}>...</article>);
418
+ `;
419
+ }
420
+ function solid(opts) {
421
+ const url = opts.url ?? DEFAULT_URL;
422
+ const exp = expandLiteral(opts.expand);
423
+ return `import { createSignal, onCleanup } from "solid-js";
424
+ import { createCmsClient, type Entry } from "trellis/cms";
425
+
426
+ const cms = ${clientLiteral(url, opts.directory)};
427
+
428
+ export function create${pascal(opts.collection)}() {
429
+ const [entries, setEntries] = createSignal<Entry[]>([]);
430
+ const off = cms.collection("${opts.collection}").subscribe(setEntries, {${exp}});
431
+ onCleanup(off);
432
+ return entries;
433
+ }
434
+
435
+ // Usage in a component:
436
+ // const posts = create${pascal(opts.collection)}();
437
+ // return <For each={posts()}>{(p) => <article>...</article>}</For>;
438
+ `;
439
+ }
440
+ function vue(opts) {
441
+ const url = opts.url ?? DEFAULT_URL;
442
+ const exp = expandLiteral(opts.expand);
443
+ return `import { ref, onUnmounted } from "vue";
444
+ import { createCmsClient, type Entry } from "trellis/cms";
445
+
446
+ const cms = ${clientLiteral(url, opts.directory)};
447
+
448
+ export function use${pascal(opts.collection)}() {
449
+ const entries = ref<Entry[]>([]);
450
+ const off = cms.collection("${opts.collection}").subscribe((next) => {
451
+ entries.value = next;
452
+ }, {${exp}});
453
+ onUnmounted(off);
454
+ return entries;
455
+ }
456
+ `;
457
+ }
458
+ function pascal(s) {
459
+ return s.split(/[_\-\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
460
+ }
461
+ function scaffoldConsumer(opts) {
462
+ switch (opts.framework ?? "vanilla") {
463
+ case "react":
464
+ return react(opts);
465
+ case "solid":
466
+ return solid(opts);
467
+ case "vue":
468
+ return vue(opts);
469
+ default:
470
+ return vanilla(opts);
471
+ }
472
+ }
473
+ function scaffoldFilename(opts) {
474
+ const base = `cms-${opts.collection.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
475
+ switch (opts.framework ?? "vanilla") {
476
+ case "react":
477
+ case "solid":
478
+ return `${base}.ts`;
479
+ case "vue":
480
+ return `${base}.ts`;
481
+ default:
482
+ return `${base}.js`;
483
+ }
484
+ }
485
+ export {
486
+ scaffoldFilename,
487
+ scaffoldConsumer,
488
+ createCmsClient,
489
+ EntryRef,
490
+ CollectionRef,
491
+ CmsClient
492
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Trellis CMS — internal helpers (fact join, status detection, reference expansion).
3
+ */
4
+ import type { Entry } from './types.js';
5
+ export type RawFact = {
6
+ e: string;
7
+ a: string;
8
+ v: string | number | boolean;
9
+ };
10
+ export type RawEntity = {
11
+ id: string;
12
+ type: string;
13
+ };
14
+ export type RawLink = {
15
+ e1: string;
16
+ a: string;
17
+ e2: string;
18
+ };
19
+ export declare function isSystemType(type: string): boolean;
20
+ export declare function typeKey(type: string): string;
21
+ export declare function entryFromFacts(entity: RawEntity, facts: RawFact[], links?: RawLink[]): Entry;
22
+ export declare function groupFactsByEntity(facts: RawFact[]): Map<string, RawFact[]>;
23
+ export declare function groupLinksBySource(links: RawLink[]): Map<string, RawLink[]>;
24
+ /**
25
+ * For each entry, replace string ids in `expand` field keys with the resolved Entry.
26
+ * Lookups are batched in parallel.
27
+ */
28
+ export declare function expandReferences(entries: Entry[], expandKeys: string[], fetchEntity: (id: string) => Promise<Entry | null>): Promise<Entry[]>;
29
+ /**
30
+ * Cheap deep-equality fingerprint for change detection in subscriptions.
31
+ */
32
+ export declare function fingerprint(value: unknown): string;
33
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../../src/cms/internal.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,YAAY,CAAC;AAErD,MAAM,MAAM,OAAO,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,OAAO,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAwB5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,GAAG,KAAK,CA+B5F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAQ3E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAQ3E;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,KAAK,EAAE,EAChB,UAAU,EAAE,MAAM,EAAE,EACpB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GACjD,OAAO,CAAC,KAAK,EAAE,CAAC,CA+BlB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAElD"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Scaffold generators for CMS consumer code.
3
+ *
4
+ * Used by the agent's `cms` tool (scaffold_consumer action) to write
5
+ * starter integration files instead of generating static data dumps.
6
+ */
7
+ import type { Framework } from './types.js';
8
+ export type ScaffoldOptions = {
9
+ collection: string;
10
+ framework?: Framework;
11
+ /** Field keys to expand as references (e.g. ["author"]). */
12
+ expand?: string[];
13
+ /** Override the CMS server URL. Default: http://localhost:4096 */
14
+ url?: string;
15
+ /**
16
+ * Project directory for multi-instance backends (e.g. opencode requires this).
17
+ * Baked into the generated client literal so requests route to the correct project.
18
+ */
19
+ directory?: string;
20
+ };
21
+ export declare function scaffoldConsumer(opts: ScaffoldOptions): string;
22
+ export declare function scaffoldFilename(opts: ScaffoldOptions): string;
23
+ //# sourceMappingURL=scaffold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cms/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA6GF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAW9D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAW9D"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Trellis CMS Client — public types
3
+ *
4
+ * @module trellis/cms
5
+ */
6
+ export type EntryStatus = 'draft' | 'published';
7
+ export type Entry<T extends Record<string, unknown> = Record<string, unknown>> = {
8
+ /** Stable entity id (e.g. "blog_post:abc123" or "BlogPost:abc"). */
9
+ id: string;
10
+ /** Original type string as stored on the entity (e.g. "BlogPost"). */
11
+ type: string;
12
+ /** "draft" or "published". Missing cms_status fact is treated as "draft". */
13
+ status: EntryStatus;
14
+ /** All other facts as a flat field bag. Reference fields hold entity ids until expanded. */
15
+ fields: T;
16
+ };
17
+ export type ListOptions = {
18
+ /** Filter by status. Default: "published". Use "all" to include drafts. */
19
+ status?: 'all' | EntryStatus;
20
+ /** Field keys to resolve as references — string ids become nested Entry objects. */
21
+ expand?: string[];
22
+ /** Max results. Default: 100. */
23
+ limit?: number;
24
+ };
25
+ export type GetOptions = {
26
+ expand?: string[];
27
+ };
28
+ export type SubscribeExtras = {
29
+ /** Called when a poll fails. If absent, errors are silently swallowed. */
30
+ onError?: (err: unknown) => void;
31
+ /**
32
+ * Custom equality check between consecutive results. Default: deep JSON
33
+ * comparison. Override with a faster check (e.g. id/version-based) for
34
+ * very large payloads.
35
+ */
36
+ equals?: (prev: unknown, next: unknown) => boolean;
37
+ };
38
+ export type ListSubscribeOptions = ListOptions & SubscribeExtras;
39
+ export type EntrySubscribeOptions = GetOptions & SubscribeExtras;
40
+ export type Unsubscribe = () => void;
41
+ export type ListSubscriber<T extends Record<string, unknown> = Record<string, unknown>> = (entries: Entry<T>[]) => void;
42
+ export type EntrySubscriber<T extends Record<string, unknown> = Record<string, unknown>> = (entry: Entry<T> | null) => void;
43
+ export type Collection = {
44
+ /** Normalized lowercase key (e.g. "blog_post"). */
45
+ key: string;
46
+ /** Human-readable label. */
47
+ label: string;
48
+ /** True if no explicit TypeSchema exists — schema is inferred from existing entries. */
49
+ inferred: boolean;
50
+ /** Total entry count (all statuses). */
51
+ count: number;
52
+ };
53
+ export type CmsClientOptions = {
54
+ /** Base URL of the Trellis-compatible HTTP server (e.g. opencode at "http://localhost:4096"). */
55
+ url: string;
56
+ /**
57
+ * Path prefix for store routes. Default: "/trellis/store" (matches opencode).
58
+ * Override only if pointing at a different server layout.
59
+ */
60
+ basePath?: string;
61
+ /**
62
+ * Project directory for multi-instance backends (opencode requires this).
63
+ * If omitted, the request goes to the default instance.
64
+ */
65
+ directory?: string;
66
+ /** Polling interval in ms for subscribe(). Minimum 500ms. Default: 2000. */
67
+ pollIntervalMs?: number;
68
+ /** Custom fetch implementation (for SSR / Node environments without global fetch). */
69
+ fetch?: typeof fetch;
70
+ /** Optional bearer token for authenticated routes. */
71
+ apiKey?: string;
72
+ };
73
+ export type Framework = 'vanilla' | 'react' | 'solid' | 'vue';
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cms/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,WAAW,CAAC;AAEhD,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IAC/E,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,WAAW,CAAC;IACpB,4FAA4F;IAC5F,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,KAAK,GAAG,WAAW,CAAC;IAC7B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;CACpD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,eAAe,CAAC;AACjE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,eAAe,CAAC;AAEjE,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AACrC,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CACxF,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAChB,IAAI,CAAC;AACV,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CACzF,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,KACnB,IAAI,CAAC;AAEV,MAAM,MAAM,UAAU,GAAG;IACvB,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iGAAiG;IACjG,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC"}
package/dist/db/index.js CHANGED
@@ -1,14 +1,5 @@
1
1
  // @bun
2
2
  import"../index-c9h37r6h.js";
3
- import {
4
- importFile,
5
- importRecords
6
- } from "../index-skhn0agf.js";
7
- import {
8
- deploy
9
- } from "../index-wt8rz4gn.js";
10
- import"../index-bmyt7k8n.js";
11
- import"../index-y6a4kj0p.js";
12
3
  import {
13
4
  ADMIN_ONLY,
14
5
  ANONYMOUS,
@@ -27,6 +18,15 @@ import {
27
18
  startServer,
28
19
  verifyJwt
29
20
  } from "../index-6n5dcebj.js";
21
+ import {
22
+ importFile,
23
+ importRecords
24
+ } from "../index-skhn0agf.js";
25
+ import {
26
+ deploy
27
+ } from "../index-wt8rz4gn.js";
28
+ import"../index-bmyt7k8n.js";
29
+ import"../index-y6a4kj0p.js";
30
30
  import"../index-n9f2qyh5.js";
31
31
  import"../index-k5b0xskw.js";
32
32
  import {
@@ -1,5 +1,23 @@
1
1
  // @bun
2
2
  import"../index-c9h37r6h.js";
3
+ import {
4
+ ADMIN_ONLY,
5
+ ANONYMOUS,
6
+ FULLY_PUBLIC,
7
+ GITHUB_PROVIDER,
8
+ GOOGLE_PROVIDER,
9
+ OWNER_ONLY,
10
+ PUBLIC_READ,
11
+ PermissionError,
12
+ PermissionRegistry,
13
+ SubscriptionManager,
14
+ buildOAuthUrl,
15
+ exchangeOAuthCode,
16
+ resolveAuth,
17
+ signJwt,
18
+ startServer,
19
+ verifyJwt
20
+ } from "../index-6n5dcebj.js";
3
21
  import {
4
22
  importFile,
5
23
  importRecords
@@ -24,24 +42,6 @@ import {
24
42
  runSpriteCopy,
25
43
  runSpriteInteractive
26
44
  } from "../index-y6a4kj0p.js";
27
- import {
28
- ADMIN_ONLY,
29
- ANONYMOUS,
30
- FULLY_PUBLIC,
31
- GITHUB_PROVIDER,
32
- GOOGLE_PROVIDER,
33
- OWNER_ONLY,
34
- PUBLIC_READ,
35
- PermissionError,
36
- PermissionRegistry,
37
- SubscriptionManager,
38
- buildOAuthUrl,
39
- exchangeOAuthCode,
40
- resolveAuth,
41
- signJwt,
42
- startServer,
43
- verifyJwt
44
- } from "../index-6n5dcebj.js";
45
45
  import"../index-n9f2qyh5.js";
46
46
  import"../index-xzym9w0m.js";
47
47
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trellis",
3
- "version": "3.0.3",
3
+ "version": "3.1.3",
4
4
  "description": "Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -59,6 +59,11 @@
59
59
  "bun": "./dist/links/index.js",
60
60
  "import": "./dist/links/index.js"
61
61
  },
62
+ "./cms": {
63
+ "types": "./dist/cms/index.d.ts",
64
+ "bun": "./dist/cms/index.js",
65
+ "import": "./dist/cms/index.js"
66
+ },
62
67
  "./decisions": {
63
68
  "types": "./dist/decisions/index.d.ts",
64
69
  "bun": "./dist/decisions/index.js",
@@ -94,6 +99,7 @@
94
99
  "logo.png",
95
100
  "logo.svg",
96
101
  "LICENSE",
102
+ "CHANGELOG.md",
97
103
  "README.md"
98
104
  ],
99
105
  "scripts": {
@@ -101,9 +107,9 @@
101
107
  "cli": "bun run src/cli/index.ts",
102
108
  "mcp": "bun run src/mcp/index.ts",
103
109
  "mcp:docs": "bun run src/mcp/docs.ts",
104
- "build": "bun build src/index.ts src/core/index.ts src/vcs/index.ts src/embeddings/index.ts src/links/index.ts src/decisions/index.ts src/server/index.ts src/client/index.ts src/react/index.ts src/db/index.ts src/cli/index.ts --outdir dist --target bun --splitting --format esm --root src --external @xenova/transformers --external @huggingface/transformers --external react && mkdir -p dist/ui && cp src/ui/client.html dist/ui/client.html && tsc -p tsconfig.build.json --emitDeclarationOnly --noEmit false --noEmitOnError false && bun run build:inspector",
110
+ "build": "bun build src/index.ts src/core/index.ts src/vcs/index.ts src/embeddings/index.ts src/links/index.ts src/decisions/index.ts src/server/index.ts src/client/index.ts src/react/index.ts src/db/index.ts src/cli/index.ts src/cms/index.ts --outdir dist --target bun --splitting --format esm --root src --external @xenova/transformers --external @huggingface/transformers --external react && mkdir -p dist/ui && cp src/ui/client.html dist/ui/client.html && tsc -p tsconfig.build.json --emitDeclarationOnly --noEmit false --noEmitOnError false && bun run build:inspector",
105
111
  "build:inspector": "vite build --config vite.inspector.config.ts",
106
- "test": "bun test test/core test/vcs test/git test/p2 test/p3 test/p4 test/p5 test/p6 test/p7 test/engine.test.ts",
112
+ "test": "bun test test/core test/cms test/vcs test/git test/p2 test/p3 test/p4 test/p5 test/p6 test/p7 test/engine.test.ts",
107
113
  "test:all": "bun test",
108
114
  "prepublishOnly": "npm run test && npm run build"
109
115
  },