@treeseed/sdk 0.1.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.
Files changed (63) hide show
  1. package/README.md +565 -0
  2. package/dist/cli-tools.js +44 -0
  3. package/dist/content-store.js +237 -0
  4. package/dist/d1-store.js +549 -0
  5. package/dist/frontmatter.js +33 -0
  6. package/dist/git-runtime.js +67 -0
  7. package/dist/index.js +12 -0
  8. package/dist/model-registry.js +164 -0
  9. package/dist/runtime.js +36 -0
  10. package/dist/scripts/.ts-run-1775616845195-odh4xzphk3l.js +22 -0
  11. package/dist/scripts/.ts-run-1775616848931-9386s6kwrl.js +126 -0
  12. package/dist/scripts/assert-release-tag-version.d.ts +1 -0
  13. package/dist/scripts/assert-release-tag-version.js +23 -0
  14. package/dist/scripts/build-dist.d.ts +1 -0
  15. package/dist/scripts/build-dist.js +114 -0
  16. package/dist/scripts/package-tools.d.ts +15 -0
  17. package/dist/scripts/package-tools.js +76 -0
  18. package/dist/scripts/publish-package.d.ts +1 -0
  19. package/dist/scripts/publish-package.js +20 -0
  20. package/dist/scripts/release-verify.d.ts +1 -0
  21. package/dist/scripts/release-verify.js +49 -0
  22. package/dist/scripts/run-ts.js +45 -0
  23. package/dist/scripts/test-smoke.d.ts +1 -0
  24. package/dist/scripts/test-smoke.js +77 -0
  25. package/dist/sdk-filters.js +77 -0
  26. package/dist/sdk-types.js +24 -0
  27. package/dist/sdk.js +232 -0
  28. package/dist/src/cli-tools.d.ts +3 -0
  29. package/dist/src/content-store.d.ts +24 -0
  30. package/dist/src/d1-store.d.ts +108 -0
  31. package/dist/src/frontmatter.d.ts +6 -0
  32. package/dist/src/git-runtime.d.ts +16 -0
  33. package/dist/src/index.d.ts +6 -0
  34. package/dist/src/model-registry.d.ts +4 -0
  35. package/dist/src/runtime.d.ts +1 -0
  36. package/dist/src/sdk-filters.d.ts +4 -0
  37. package/dist/src/sdk-types.d.ts +285 -0
  38. package/dist/src/sdk.d.ts +109 -0
  39. package/dist/src/stores/cursor-store.d.ts +10 -0
  40. package/dist/src/stores/envelopes.d.ts +116 -0
  41. package/dist/src/stores/helpers.d.ts +12 -0
  42. package/dist/src/stores/lease-store.d.ts +18 -0
  43. package/dist/src/stores/message-store.d.ts +12 -0
  44. package/dist/src/stores/run-store.d.ts +10 -0
  45. package/dist/src/stores/subscription-store.d.ts +9 -0
  46. package/dist/src/types/agents.d.ts +100 -0
  47. package/dist/src/types/cloudflare.d.ts +32 -0
  48. package/dist/src/wrangler-d1.d.ts +25 -0
  49. package/dist/stores/cursor-store.js +158 -0
  50. package/dist/stores/envelopes.js +219 -0
  51. package/dist/stores/helpers.js +42 -0
  52. package/dist/stores/lease-store.js +183 -0
  53. package/dist/stores/message-store.js +249 -0
  54. package/dist/stores/run-store.js +166 -0
  55. package/dist/stores/subscription-store.js +171 -0
  56. package/dist/test/test-fixture.d.ts +1 -0
  57. package/dist/test/utils/envelopes.test.d.ts +1 -0
  58. package/dist/test/utils/sdk.test.d.ts +1 -0
  59. package/dist/types/agents.js +40 -0
  60. package/dist/types/cloudflare.js +0 -0
  61. package/dist/vitest.config.d.ts +2 -0
  62. package/dist/wrangler-d1.js +84 -0
  63. package/package.json +130 -0
package/README.md ADDED
@@ -0,0 +1,565 @@
1
+ # `@treeseed/sdk`
2
+
3
+ `@treeseed/sdk` is the standalone TreeSeed SDK for content-backed and D1-backed object models.
4
+
5
+ It exposes the public model and storage surface used by TreeSeed agents and supporting tooling:
6
+
7
+ - content-backed access for pages, notes, questions, objectives, people, agents, books, and knowledge
8
+ - D1-backed access for subscriptions, messages, agent runs, cursors, and content leases
9
+ - stable query and mutation APIs for `get`, `read`, `search`, `follow`, `pick`, `create`, and `update`
10
+
11
+ ## Consumer Contract
12
+
13
+ - Node `>=20`
14
+ - ESM package
15
+ - install from npm as a normal package dependency
16
+ - import from the package root or documented subpath exports
17
+
18
+ Install:
19
+
20
+ ```bash
21
+ npm install @treeseed/sdk
22
+ ```
23
+
24
+ Example:
25
+
26
+ ```ts
27
+ import { AgentSdk } from '@treeseed/sdk';
28
+
29
+ const sdk = new AgentSdk();
30
+ ```
31
+
32
+ ## Using The SDK In Applications
33
+
34
+ `AgentSdk` is the main application entrypoint. It routes each request to either the content-backed store or the D1-backed store based on the model you ask for, and it always returns a JSON envelope with:
35
+
36
+ - `ok`
37
+ - `model`
38
+ - `operation`
39
+ - `payload`
40
+ - optional `meta`
41
+
42
+ For most application code, the working pattern is:
43
+
44
+ 1. create one SDK instance for your process, request handler, worker, or job
45
+ 2. call `get`, `read`, `search`, `follow`, `pick`, `create`, or `update`
46
+ 3. read the typed `payload` from the returned envelope
47
+
48
+ ### Initialize An SDK Instance
49
+
50
+ Use the default constructor when you want in-memory D1 behavior and a content root resolved from your environment or working directory:
51
+
52
+ ```ts
53
+ import { AgentSdk } from '@treeseed/sdk';
54
+
55
+ const sdk = new AgentSdk({
56
+ repoRoot: '/absolute/path/to/your-site',
57
+ });
58
+ ```
59
+
60
+ Use `createLocal()` when you want a local Wrangler-backed D1 database:
61
+
62
+ ```ts
63
+ import { AgentSdk } from '@treeseed/sdk';
64
+
65
+ const sdk = AgentSdk.createLocal({
66
+ repoRoot: '/absolute/path/to/your-site',
67
+ databaseName: 'treeseed-local',
68
+ persistTo: '.wrangler/state/v3/d1',
69
+ });
70
+ ```
71
+
72
+ Use `MemoryAgentDatabase` explicitly in tests or scripts when you want a fully in-memory setup:
73
+
74
+ ```ts
75
+ import { AgentSdk } from '@treeseed/sdk';
76
+ import { MemoryAgentDatabase } from '@treeseed/sdk/d1-store';
77
+
78
+ const sdk = new AgentSdk({
79
+ repoRoot: '/absolute/path/to/your-site',
80
+ database: new MemoryAgentDatabase(),
81
+ });
82
+ ```
83
+
84
+ ### Read A Single Record
85
+
86
+ Use `get()` when you want one record by `id`, `slug`, or `key`.
87
+
88
+ ```ts
89
+ const response = await sdk.get({
90
+ model: 'knowledge',
91
+ slug: 'guides/getting-started/1-introduction',
92
+ });
93
+
94
+ if (response.payload) {
95
+ console.log(response.payload.title);
96
+ console.log(response.payload.body);
97
+ }
98
+ ```
99
+
100
+ Use `read()` when you want the same lookup behavior but want the returned envelope to say `operation: 'read'`.
101
+
102
+ ```ts
103
+ const response = await sdk.read({
104
+ model: 'page',
105
+ slug: 'getting-started',
106
+ });
107
+ ```
108
+
109
+ ### Search Across A Model
110
+
111
+ Use `search()` to list and filter records from a model.
112
+
113
+ ```ts
114
+ const response = await sdk.search({
115
+ model: 'knowledge',
116
+ filters: [
117
+ { field: 'title', op: 'contains', value: 'TreeSeed' },
118
+ { field: 'tags', op: 'contains', value: 'onboarding' },
119
+ ],
120
+ sort: [{ field: 'updated', direction: 'desc' }],
121
+ limit: 10,
122
+ });
123
+
124
+ console.log(response.meta?.count);
125
+ console.log(response.payload.map((item) => item.slug));
126
+ ```
127
+
128
+ `search()` is the main method for application reads such as:
129
+
130
+ - listing recent notes
131
+ - finding objectives by status
132
+ - finding people by role or affiliation
133
+ - finding queued D1 messages by type or status
134
+
135
+ ### Follow Changes Since A Timestamp
136
+
137
+ Use `follow()` when your application wants records changed since a known point in time.
138
+
139
+ ```ts
140
+ const response = await sdk.follow({
141
+ model: 'knowledge',
142
+ since: '2026-04-07T00:00:00.000Z',
143
+ filters: [{ field: 'tags', op: 'contains', value: 'treeseed' }],
144
+ });
145
+
146
+ for (const item of response.payload.items) {
147
+ console.log(item.slug);
148
+ }
149
+ ```
150
+
151
+ The payload shape is:
152
+
153
+ ```ts
154
+ {
155
+ items: [...],
156
+ since: '...'
157
+ }
158
+ ```
159
+
160
+ ### Pick Work Items
161
+
162
+ Use `pick()` when you want the SDK to choose one item from a model for a worker.
163
+
164
+ For content-backed models, `pick()` tries to claim a content lease and returns both the selected item and a `leaseToken` when a claim succeeds.
165
+
166
+ ```ts
167
+ const response = await sdk.pick({
168
+ model: 'objective',
169
+ workerId: 'planner-1',
170
+ leaseSeconds: 300,
171
+ filters: [{ field: 'status', op: 'eq', value: 'in progress' }],
172
+ });
173
+
174
+ if (response.payload.item) {
175
+ console.log(response.payload.item.slug);
176
+ console.log(response.payload.leaseToken);
177
+ }
178
+ ```
179
+
180
+ For `message`, `pick()` routes to queue claiming behavior in the D1 layer.
181
+
182
+ ### Create Content Or D1 Records
183
+
184
+ Use `create()` for models that support creation.
185
+
186
+ For content-backed models, pass frontmatter-like fields in `data`. The SDK writes the document and returns the created item plus git metadata.
187
+
188
+ ```ts
189
+ const response = await sdk.create({
190
+ model: 'note',
191
+ actor: 'app-server',
192
+ data: {
193
+ slug: 'operating-a-small-treeseed',
194
+ title: 'Operating a Small TreeSeed',
195
+ status: 'live',
196
+ author: 'TreeSeed Team',
197
+ tags: ['operations', 'treeseed'],
198
+ body: 'Keep the content model simple and the workflows visible.',
199
+ },
200
+ });
201
+
202
+ console.log(response.payload.item.slug);
203
+ console.log(response.payload.git);
204
+ ```
205
+
206
+ For D1-backed models, `create()` delegates to the relevant store and returns the created entity.
207
+
208
+ ### Update Existing Records
209
+
210
+ Use `update()` when you want to modify an existing content-backed or D1-backed record.
211
+
212
+ ```ts
213
+ const response = await sdk.update({
214
+ model: 'objective',
215
+ slug: 'make-the-sample-site-easy-to-operate',
216
+ actor: 'app-server',
217
+ data: {
218
+ status: 'live',
219
+ body: 'The objective is now complete and documented.',
220
+ },
221
+ });
222
+ ```
223
+
224
+ For content-backed models, `update()` returns the updated item and git metadata. For D1-backed models, it returns the updated row or `null` when no matching record exists.
225
+
226
+ ### Work With Messages
227
+
228
+ The SDK exposes dedicated queue helpers in addition to generic model access.
229
+
230
+ Create a message:
231
+
232
+ ```ts
233
+ const created = await sdk.createMessage({
234
+ type: 'guidance_ready',
235
+ actor: 'guide-agent',
236
+ payload: {
237
+ slug: 'guides/getting-started/1-introduction',
238
+ },
239
+ relatedModel: 'knowledge',
240
+ relatedId: 'guides/getting-started/1-introduction',
241
+ priority: 5,
242
+ maxAttempts: 3,
243
+ });
244
+ ```
245
+
246
+ Claim a message:
247
+
248
+ ```ts
249
+ const claimed = await sdk.claimMessage({
250
+ workerId: 'worker-1',
251
+ messageTypes: ['guidance_ready'],
252
+ leaseSeconds: 300,
253
+ });
254
+ ```
255
+
256
+ Acknowledge a message:
257
+
258
+ ```ts
259
+ await sdk.ackMessage({
260
+ id: 1,
261
+ status: 'completed',
262
+ });
263
+ ```
264
+
265
+ ### Record Agent Runs, Cursors, And Leases
266
+
267
+ Record a run:
268
+
269
+ ```ts
270
+ await sdk.recordRun({
271
+ run: {
272
+ runId: 'run-123',
273
+ agentSlug: 'guide-agent',
274
+ status: 'completed',
275
+ triggerSource: 'message',
276
+ startedAt: '2026-04-07T00:00:00.000Z',
277
+ finishedAt: '2026-04-07T00:05:00.000Z',
278
+ },
279
+ });
280
+ ```
281
+
282
+ Read and update a cursor:
283
+
284
+ ```ts
285
+ const cursor = await sdk.getCursor({
286
+ agentSlug: 'guide-agent',
287
+ cursorKey: 'knowledge-sync',
288
+ });
289
+
290
+ await sdk.upsertCursor({
291
+ agentSlug: 'guide-agent',
292
+ cursorKey: 'knowledge-sync',
293
+ cursorValue: '2026-04-07T00:00:00.000Z',
294
+ });
295
+ ```
296
+
297
+ Release one lease or all leases:
298
+
299
+ ```ts
300
+ await sdk.releaseLease({
301
+ model: 'objective',
302
+ itemKey: 'make-the-sample-site-easy-to-operate',
303
+ leaseToken: 'lease-token',
304
+ });
305
+
306
+ await sdk.releaseAllLeases();
307
+ ```
308
+
309
+ ### How TreeSeed Uses Agent Runs, Cursors, And Leases
310
+
311
+ These three concepts are the operational state layer for TreeSeed's agent runtime. They are not general content models like `page` or `knowledge`. Instead, they let TreeSeed coordinate ongoing agent work safely and make that work inspectable after the fact.
312
+
313
+ #### Agent Runs
314
+
315
+ An `agent_run` is the execution trace for one agent invocation.
316
+
317
+ TreeSeed records a run when the agent kernel starts an agent, and records it again when the run finishes or fails. In practice, that means a run captures:
318
+
319
+ - which agent ran
320
+ - what triggered it
321
+ - the current status such as `running`, `completed`, `failed`, or `waiting`
322
+ - which message or item was selected
323
+ - summary or error output
324
+ - optional branch, commit, PR, and changed-path metadata
325
+ - start and finish timestamps
326
+
327
+ In the TreeSeed agent runtime, [`agent/src/agents/kernel/agent-kernel.ts`](/home/adrian/Projects/treeseed/agent/src/agents/kernel/agent-kernel.ts) calls `sdk.recordRun()` at the beginning of execution and again after the handler returns. That gives TreeSeed a durable per-run audit trail for:
328
+
329
+ - debugging agent behavior
330
+ - understanding why an agent did or did not run
331
+ - inspecting outputs from planner, reviewer, notifier, and similar handlers
332
+ - connecting downstream events back to the run that produced them
333
+
334
+ Conceptually, `agent_run` is the answer to: "What happened during this agent invocation?"
335
+
336
+ #### Agent Cursors
337
+
338
+ An `agent_cursor` is a tiny per-agent checkpoint. It stores one named progress marker as:
339
+
340
+ - `agentSlug`
341
+ - `cursorKey`
342
+ - `cursorValue`
343
+
344
+ TreeSeed uses cursors to remember where an agent last left off, so the next cycle can resume from the correct point instead of starting over.
345
+
346
+ In the runtime, cursors are used in a few concrete ways:
347
+
348
+ - the agent kernel writes `last_run_at` after a successful run
349
+ - follow triggers read a cursor like `follow:model-a,model-b` to know which timestamp to compare against
350
+ - the sample planner agent writes `last_priority_run_at`
351
+ - the sample notifier agent reads and updates `last_notified_at` so it only announces new activity
352
+
353
+ You can see that usage in:
354
+
355
+ - [`agent/src/agents/kernel/agent-kernel.ts`](/home/adrian/Projects/treeseed/agent/src/agents/kernel/agent-kernel.ts)
356
+ - [`agent/src/agents/kernel/trigger-resolver.ts`](/home/adrian/Projects/treeseed/agent/src/agents/kernel/trigger-resolver.ts)
357
+ - [`core/fixture/src/agents/planner.ts`](/home/adrian/Projects/treeseed/core/fixture/src/agents/planner.ts)
358
+ - [`core/fixture/src/agents/notifier.ts`](/home/adrian/Projects/treeseed/core/fixture/src/agents/notifier.ts)
359
+
360
+ Conceptually, `agent_cursor` is the answer to: "Where should this agent continue from next time?"
361
+
362
+ #### Content Leases
363
+
364
+ A `content_lease` is a short-lived claim on one content item. TreeSeed uses leases to avoid two workers picking and mutating the same item at the same time.
365
+
366
+ When `pick()` runs against a content-backed model, the SDK does not just choose the "best" item. It also tries to claim a lease in the database. If another worker already holds a live lease for that item, the claim fails and the picker moves on.
367
+
368
+ Each lease stores:
369
+
370
+ - the model
371
+ - the item key
372
+ - who claimed it
373
+ - when it was claimed
374
+ - when the lease expires
375
+ - a lease token
376
+
377
+ This is how TreeSeed prevents duplicate work during autonomous or parallel agent execution, especially for content-backed tasks like selecting the next note, question, or objective to act on.
378
+
379
+ In the SDK, the lease flow is wired through:
380
+
381
+ - content selection in [`sdk/src/content-store.ts`](/home/adrian/Projects/treeseed/sdk/src/content-store.ts)
382
+ - D1 lease persistence in [`sdk/src/stores/lease-store.ts`](/home/adrian/Projects/treeseed/sdk/src/stores/lease-store.ts)
383
+ - runtime cleanup through `releaseLease()` and `releaseAllLeases()`
384
+
385
+ The agent kernel exposes that cleanup path as `releaseLeases()` so TreeSeed operators can clear stale claims when needed.
386
+
387
+ Conceptually, `content_lease` is the answer to: "Who currently owns this piece of work, and when does that claim expire?"
388
+
389
+ #### How They Work Together
390
+
391
+ In TreeSeed, these records solve different parts of the same runtime problem:
392
+
393
+ - `agent_run` records what happened
394
+ - `agent_cursor` records where to resume
395
+ - `content_lease` records who currently owns a piece of work
396
+
397
+ Together, they make the agent system:
398
+
399
+ - inspectable, because each run leaves a trace
400
+ - incremental, because agents can continue from saved cursors
401
+ - concurrency-safe, because content picking is guarded by leases
402
+
403
+ That combination is what lets TreeSeed move from one-off scripts toward a manageable long-running agent runtime.
404
+
405
+ ### Discover Agent Specs
406
+
407
+ When your application stores agent definitions in content, use `listRawAgentSpecs()` or `listAgentSpecs()`.
408
+
409
+ ```ts
410
+ const specs = await sdk.listAgentSpecs({ enabled: true });
411
+
412
+ for (const spec of specs) {
413
+ console.log(spec.slug, spec.handler);
414
+ }
415
+ ```
416
+
417
+ Use `listRawAgentSpecs()` when you want the underlying content entries. Use `listAgentSpecs()` when you want normalized runtime specs.
418
+
419
+ ### Restrict Access With `ScopedAgentSdk`
420
+
421
+ Use `scopeForAgent()` when you want application code to enforce an agent’s declared permissions before executing SDK operations.
422
+
423
+ ```ts
424
+ const scoped = sdk.scopeForAgent({
425
+ slug: 'guide-agent',
426
+ permissions: [
427
+ { model: 'knowledge', operations: ['get', 'read', 'search'] },
428
+ { model: 'message', operations: ['create'] },
429
+ ],
430
+ });
431
+
432
+ await scoped.search({
433
+ model: 'knowledge',
434
+ filters: [{ field: 'tags', op: 'contains', value: 'treeseed' }],
435
+ });
436
+ ```
437
+
438
+ `ScopedAgentSdk` automatically injects the agent slug as `actor` for `create()`, `update()`, and `createMessage()`, and throws when the requested operation is not allowed.
439
+
440
+ ### Model And Filter Notes
441
+
442
+ The SDK resolves model aliases for you. For example, `docs` maps to `knowledge` and `people` maps to `person`.
443
+
444
+ Common request fields:
445
+
446
+ - `model`: the canonical model or an accepted alias
447
+ - `filters`: array of `{ field, op, value }`
448
+ - `sort`: array of `{ field, direction }`
449
+ - `limit`: max number of returned items
450
+
451
+ Common filter operators include:
452
+
453
+ - `eq`
454
+ - `in`
455
+ - `contains`
456
+ - `prefix`
457
+ - `gt`
458
+ - `gte`
459
+ - `lt`
460
+ - `lte`
461
+ - `updated_since`
462
+ - `related_to`
463
+
464
+ ### Envelope Pattern
465
+
466
+ Every top-level SDK call returns a consistent envelope:
467
+
468
+ ```ts
469
+ const response = await sdk.search({ model: 'person', limit: 1 });
470
+
471
+ response.ok; // true
472
+ response.model; // 'person'
473
+ response.operation; // 'search'
474
+ response.payload; // typed result
475
+ response.meta; // optional metadata
476
+ ```
477
+
478
+ That envelope shape makes it straightforward to use the SDK in API handlers, background jobs, CLIs, and agent runtimes without introducing a second application-specific response format.
479
+
480
+ ## Public Surface
481
+
482
+ The package root exports the main SDK class, model registry helpers, CLI option helpers, and shared SDK types.
483
+
484
+ The package also exposes focused subpaths including:
485
+
486
+ - `@treeseed/sdk/sdk`
487
+ - `@treeseed/sdk/content-store`
488
+ - `@treeseed/sdk/d1-store`
489
+ - `@treeseed/sdk/frontmatter`
490
+ - `@treeseed/sdk/git-runtime`
491
+ - `@treeseed/sdk/models`
492
+ - `@treeseed/sdk/sdk-filters`
493
+ - `@treeseed/sdk/cli-tools`
494
+ - `@treeseed/sdk/types`
495
+ - `@treeseed/sdk/types/agents`
496
+ - `@treeseed/sdk/types/cloudflare`
497
+ - `@treeseed/sdk/wrangler-d1`
498
+ - `@treeseed/sdk/stores/*`
499
+
500
+ ## Content Root Resolution
501
+
502
+ Content-backed operations need a repository root that contains `src/content`.
503
+
504
+ `AgentSdk` resolves that root in this order:
505
+
506
+ 1. the explicit `repoRoot` option
507
+ 2. `TREESEED_SDK_CONTENT_ROOT`
508
+ 3. `TREESEED_SDK_REPO_ROOT`
509
+ 4. auto-detection from the current working directory
510
+
511
+ For package-local tests and fixture-driven development, the SDK also recognizes a package fixture root containing `fixture/src/content`.
512
+
513
+ Example with an explicit root:
514
+
515
+ ```ts
516
+ import { AgentSdk } from '@treeseed/sdk';
517
+
518
+ const sdk = new AgentSdk({
519
+ repoRoot: '/absolute/path/to/site-or-fixture-root',
520
+ });
521
+ ```
522
+
523
+ ## Local Development
524
+
525
+ ```bash
526
+ npm ci
527
+ npm run build
528
+ npm test
529
+ npm run test:smoke
530
+ npm run verify
531
+ ```
532
+
533
+ What each command does:
534
+
535
+ - `npm run build`: builds `dist/`
536
+ - `npm test`: runs unit tests
537
+ - `npm run test:smoke`: packs the SDK tarball and verifies a clean import from the packed install
538
+ - `npm run verify`: runs the release verification path used by CI
539
+
540
+ ## Sample Fixture Site
541
+
542
+ The canonical SDK sample fixture lives at `../fixtures/fixture-sdk-sample-site/template` in the TreeSeed workspace.
543
+
544
+ The SDK package also keeps a mirrored local fixture at `sdk/fixture` so the standalone SDK repository can run its own tests and CI without depending on the larger workspace layout.
545
+
546
+ It serves three purposes at once:
547
+
548
+ - a small documentation surface about working with TreeSeed
549
+ - the default local test ground for content-backed SDK behavior
550
+ - a concrete example of a valid `repoRoot` for `AgentSdk`
551
+
552
+ You can point the SDK at it directly:
553
+
554
+ ```ts
555
+ import path from 'node:path';
556
+ import { AgentSdk } from '@treeseed/sdk';
557
+
558
+ const sdk = new AgentSdk({
559
+ repoRoot: path.resolve('../fixtures/fixture-sdk-sample-site/template'),
560
+ });
561
+ ```
562
+
563
+ The fixture includes representative entries for pages, notes, questions, objectives, books, knowledge, people, and agents so local queries behave like a small real site instead of a synthetic stub.
564
+
565
+ In the full TreeSeed workspace, tests prefer the workspace fixture under `fixtures/`. In the standalone SDK repository, tests fall back to `sdk/fixture`.
@@ -0,0 +1,44 @@
1
+ import {
2
+ AGENT_CLI_ALLOW_TOOLS
3
+ } from "./types/agents.js";
4
+ const ALLOWED_TOOL_SET = new Set(AGENT_CLI_ALLOW_TOOLS);
5
+ function normalizeStringArray(value, field) {
6
+ if (value === void 0 || value === null) {
7
+ return [];
8
+ }
9
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
10
+ throw new Error(`Invalid agent cli.${field}: expected an array of strings.`);
11
+ }
12
+ return value.map(String);
13
+ }
14
+ function normalizeAgentCliOptions(input) {
15
+ if (input === void 0 || input === null) {
16
+ return {
17
+ allowTools: [],
18
+ additionalArgs: []
19
+ };
20
+ }
21
+ if (typeof input !== "object" || Array.isArray(input)) {
22
+ throw new Error("Invalid agent cli config: expected an object.");
23
+ }
24
+ const cli = input;
25
+ const rawAllowTools = normalizeStringArray(cli.allowTools, "allowTools");
26
+ const invalidTools = rawAllowTools.filter((tool) => !ALLOWED_TOOL_SET.has(tool));
27
+ if (invalidTools.length) {
28
+ throw new Error(
29
+ `Invalid agent cli.allowTools entries: ${invalidTools.join(", ")}. Allowed tools: ${AGENT_CLI_ALLOW_TOOLS.join(", ")}.`
30
+ );
31
+ }
32
+ return {
33
+ model: typeof cli.model === "string" ? cli.model : void 0,
34
+ allowTools: [...new Set(rawAllowTools)],
35
+ additionalArgs: normalizeStringArray(cli.additionalArgs, "additionalArgs")
36
+ };
37
+ }
38
+ function buildCopilotAllowToolArgs(allowTools = []) {
39
+ return (allowTools ?? []).flatMap((tool) => ["--allow-tool", tool]);
40
+ }
41
+ export {
42
+ buildCopilotAllowToolArgs,
43
+ normalizeAgentCliOptions
44
+ };