@yaklabs-ai/plugin-sdk 0.13.5 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/time.d.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Temporal as Temporal_2 } from '@js-temporal/polyfill';
2
+
3
+ /**
4
+ * The id ICU resolves for an IANA time-zone id, or `undefined` when `id` is not one.
5
+ *
6
+ * Accepts `UTC`, `GMT`, and any id that contains `/` and that ICU resolves. Rejects
7
+ * abbreviations and slash-less legacy ids (`EST`, `CET`, `EST5EDT`), offsets (`+05:30`,
8
+ * `UTC+2`), the empty string, strings over 64 characters and non-strings. ICU maps link
9
+ * names to its own canonical id (`Asia/Kolkata` to `Asia/Calcutta`), and which spelling is
10
+ * canonical depends on the engine, so compare zones with {@link sameZone}, never with `===`.
11
+ */
12
+ export declare function canonicalZone(id: unknown): string | undefined;
13
+
14
+ /** True when {@link canonicalZone} accepts `id`. */
15
+ export declare function isValidZone(id: unknown): boolean;
16
+
17
+ /**
18
+ * `app.time` / `api.time`: host facts about time. Added to `kay.app` in SDK 0.12.1 and to
19
+ * `kay.agent` with the calendar zone-of-record work: a plugin that calls it declares
20
+ * `compatibility.sdk` `^0.12.1`, so an older Bonsai refuses a directory or repository plugin at
21
+ * load instead of the call failing.
22
+ */
23
+ export declare interface KayAppTime {
24
+ /**
25
+ * Reads the daemon's clock and the user's time zone now.
26
+ *
27
+ * Every call asks the host again and returns a new object; nothing is cached. Call it where you
28
+ * need the value (inside a tool, a handler or a workflow step), not inside `activate()`: a value
29
+ * read there is as old as the activation, and the host can run a bundled plugin's `activate()`
30
+ * against a recording api that has no `time`, so a read there throws. The exception is a
31
+ * directory plugin that declares a workflow with a cron `timezone`: `workflows.define` is legal
32
+ * only during `activate()`, so it reads the zone there, and the schedule then keeps that zone.
33
+ * Keep the result for as long as you use it, and not across a long wait (a sleep, a timer, a
34
+ * queued run): the zone can change while you wait. On a daemon that runs on the user's machine,
35
+ * a change of the process's `TZ` shows in the next call. A `TZ` that names a zone wins over the
36
+ * operating system's zone, so a change of the operating-system zone does not show while the
37
+ * process has one. When `TZ` is unset or does not name a zone, a change of the operating-system
38
+ * zone is meant to show, but none has been seen on a real machine yet.
39
+ *
40
+ * A zone the host cannot determine is an answer (`zoneSource: "utc-fallback"`), not an error.
41
+ *
42
+ * Use this to find the zone, not `Intl.DateTimeFormat().resolvedOptions().timeZone`, which can
43
+ * keep a zone the runtime read earlier, or `process.env.TZ`, which is often unset or not a zone.
44
+ * Neither says that the zone is unknown.
45
+ *
46
+ * It does not use `this`, so it is safe to destructure.
47
+ */
48
+ readonly snapshot: () => KayAppTimeSnapshot;
49
+ }
50
+
51
+ /**
52
+ * The plugin-facing time contract: the daemon's clock and the time zone the user is in,
53
+ * read together in one call.
54
+ *
55
+ * Ungated and always present, like `app.paths`. It confers no authority: a plugin runs in the
56
+ * daemon's process and can already read the clock. What the door adds is the daemon's answer
57
+ * for the USER's zone, and a label for the case where that answer is unknown.
58
+ *
59
+ * This is a leaf module. It lives beside both target SDKs (rather than inside `kay-app`) so the
60
+ * `kay.agent` SDK can expose the same door without importing the `kay.app` surface, which pulls
61
+ * that surface into every agent-facing bundle.
62
+ */
63
+ /** One reading of the daemon's clock and the user's time zone. Each call returns a new object. */
64
+ export declare interface KayAppTimeSnapshot {
65
+ /** The daemon's clock when the snapshot was read: milliseconds since the Unix epoch. */
66
+ readonly nowMs: number;
67
+ /**
68
+ * The time zone the user is in, as an IANA id in the spelling ICU resolves (`America/New_York`).
69
+ * One zone can have two ids, and ICU picks one: `Asia/Kolkata` comes back as `Asia/Calcutta`. So
70
+ * never compare zone ids with `===`: compare with `sameZone` from `@bonsai/plugin-sdk/time`. Pass
71
+ * `zone` as the `timeZone` of `Intl.DateTimeFormat` or `toLocaleString` to format a time for the
72
+ * user.
73
+ *
74
+ * When `zoneSource` is `utc-fallback` the zone is unknown, and this value is `UTC` only because a
75
+ * value is required.
76
+ */
77
+ readonly zone: string;
78
+ /**
79
+ * How the daemon found `zone`.
80
+ * - `host`: the daemon runs on the user's machine, and `zone` is the zone the daemon process
81
+ * runs in, read for this call: the process's `TZ` when that names a zone, otherwise the
82
+ * operating system's zone.
83
+ * - `utc-fallback`: the daemon does not know the user's zone. It runs on another machine (a remote
84
+ * daemon), or the machine's zone could not be read or is not an IANA id. `zone` is `UTC`, which
85
+ * here is a placeholder and not a fact about the user: do not show it to the user as their zone.
86
+ */
87
+ readonly zoneSource: "host" | "utc-fallback";
88
+ }
89
+
90
+ /** True when both ids are valid and ICU resolves them to the same zone. */
91
+ export declare function sameZone(a: unknown, b: unknown): boolean;
92
+
93
+ /**
94
+ * The Temporal API of this process: the engine's own `globalThis.Temporal` when it has one
95
+ * (Electron's runtime does, Node 24 does not), and `@js-temporal/polyfill`'s otherwise.
96
+ * `temporalImplementation` says which.
97
+ *
98
+ * It is a value and, through the namespace below, a type namespace: `Temporal.Instant` and
99
+ * `Temporal.ZonedDateTime` name both the classes and their instance types.
100
+ */
101
+ export declare const Temporal: TemporalApi;
102
+
103
+ /**
104
+ * The types of `@js-temporal/polyfill`'s `Temporal` namespace, so code can write
105
+ * `Temporal.Instant` as a type. `Now` and `DateTimeFormat` are left out on purpose: time
106
+ * that logic depends on comes from a `Clock`, and the polyfill's `Intl` export is not part of
107
+ * this API. `test/temporal.test.ts` fails when a new polyfill version adds a type that this
108
+ * list lacks.
109
+ */
110
+ export declare namespace Temporal {
111
+ export type ComparisonResult = Temporal_2.ComparisonResult;
112
+ export type RoundingMode = Temporal_2.RoundingMode;
113
+ export type AssignmentOptions = Temporal_2.AssignmentOptions;
114
+ export type DurationOptions = Temporal_2.DurationOptions;
115
+ export type ToInstantOptions = Temporal_2.ToInstantOptions;
116
+ export type ZonedDateTimeAssignmentOptions = Temporal_2.ZonedDateTimeAssignmentOptions;
117
+ export type ArithmeticOptions = Temporal_2.ArithmeticOptions;
118
+ export type DateUnit = Temporal_2.DateUnit;
119
+ export type TimeUnit = Temporal_2.TimeUnit;
120
+ export type DateTimeUnit = Temporal_2.DateTimeUnit;
121
+ export type PluralUnit<T extends Temporal_2.DateTimeUnit> = Temporal_2.PluralUnit<T>;
122
+ export type LargestUnit<T extends Temporal_2.DateTimeUnit> = Temporal_2.LargestUnit<T>;
123
+ export type SmallestUnit<T extends Temporal_2.DateTimeUnit> = Temporal_2.SmallestUnit<T>;
124
+ export type TotalUnit<T extends Temporal_2.DateTimeUnit> = Temporal_2.TotalUnit<T>;
125
+ export type ToStringPrecisionOptions = Temporal_2.ToStringPrecisionOptions;
126
+ export type ShowCalendarOption = Temporal_2.ShowCalendarOption;
127
+ export type CalendarTypeToStringOptions = Temporal_2.CalendarTypeToStringOptions;
128
+ export type ZonedDateTimeToStringOptions = Temporal_2.ZonedDateTimeToStringOptions;
129
+ export type InstantToStringOptions = Temporal_2.InstantToStringOptions;
130
+ export type DifferenceOptions<T extends Temporal_2.DateTimeUnit> = Temporal_2.DifferenceOptions<T>;
131
+ export type RoundTo<T extends Temporal_2.DateTimeUnit> = Temporal_2.RoundTo<T>;
132
+ export type DurationRoundTo = Temporal_2.DurationRoundTo;
133
+ export type DurationTotalOf = Temporal_2.DurationTotalOf;
134
+ export type DurationArithmeticOptions = Temporal_2.DurationArithmeticOptions;
135
+ export type TransitionDirection = Temporal_2.TransitionDirection;
136
+ export type DurationLike = Temporal_2.DurationLike;
137
+ export type Duration = Temporal_2.Duration;
138
+ export type Instant = Temporal_2.Instant;
139
+ export type CalendarLike = Temporal_2.CalendarLike;
140
+ export type PlainDateLike = Temporal_2.PlainDateLike;
141
+ export type PlainDate = Temporal_2.PlainDate;
142
+ export type PlainDateTimeLike = Temporal_2.PlainDateTimeLike;
143
+ export type PlainDateTime = Temporal_2.PlainDateTime;
144
+ export type PlainMonthDayLike = Temporal_2.PlainMonthDayLike;
145
+ export type PlainMonthDay = Temporal_2.PlainMonthDay;
146
+ export type PlainTimeLike = Temporal_2.PlainTimeLike;
147
+ export type PlainTime = Temporal_2.PlainTime;
148
+ export type TimeZoneLike = Temporal_2.TimeZoneLike;
149
+ export type PlainYearMonthLike = Temporal_2.PlainYearMonthLike;
150
+ export type PlainYearMonth = Temporal_2.PlainYearMonth;
151
+ export type ZonedDateTimeLike = Temporal_2.ZonedDateTimeLike;
152
+ export type ZonedDateTime = Temporal_2.ZonedDateTime;
153
+ }
154
+
155
+ declare type TemporalApi = typeof Temporal_2;
156
+
157
+ export { }
package/dist/time.js ADDED
@@ -0,0 +1,37 @@
1
+ // packages/time/dist/temporal.js
2
+ import { Temporal as Polyfill } from "@js-temporal/polyfill";
3
+ var engineTemporal = Reflect.get(globalThis, "Temporal");
4
+ var Temporal = engineTemporal ?? Polyfill;
5
+
6
+ // packages/time/dist/zones.js
7
+ var MAX_ZONE_ID_LENGTH = 64;
8
+ function hasIanaShape(id) {
9
+ if (id.length > MAX_ZONE_ID_LENGTH)
10
+ return false;
11
+ if (id.includes("/"))
12
+ return true;
13
+ const upper = id.toUpperCase();
14
+ return upper === "UTC" || upper === "GMT";
15
+ }
16
+ function canonicalZone(id) {
17
+ if (typeof id !== "string" || !hasIanaShape(id))
18
+ return void 0;
19
+ try {
20
+ return new Intl.DateTimeFormat("en-US", { timeZone: id }).resolvedOptions().timeZone;
21
+ } catch {
22
+ return void 0;
23
+ }
24
+ }
25
+ function isValidZone(id) {
26
+ return canonicalZone(id) !== void 0;
27
+ }
28
+ function sameZone(a, b) {
29
+ const left = canonicalZone(a);
30
+ return left !== void 0 && left === canonicalZone(b);
31
+ }
32
+ export {
33
+ Temporal,
34
+ canonicalZone,
35
+ isValidZone,
36
+ sameZone
37
+ };
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * The ids on a session's active branch: the leaf chain walked parent-ward from
3
5
  * the tip, which a trailing `leaf` marker moves. Empty for an empty log.
@@ -28,6 +30,31 @@ export declare function customEntriesFromEntries<T extends TranscriptSourceEntry
28
30
  readonly branch: "active" | "all";
29
31
  }): readonly T[];
30
32
 
33
+ export declare function matchesWorkBlock(coverage: WorkBlockCoverage, toolResultEntryIds: readonly string[]): boolean;
34
+
35
+ /**
36
+ * Tool names the transcript draws as their own surface, or lifts into the reply, instead of
37
+ * counting them as work in the closed bar (venue `inline` or `turn-tail`, or the parsed
38
+ * `submit_final`). A call to one of these is not a "visible call". The renderer's built-in tool
39
+ * registrations are the source of truth; its parity test fails when this list drifts from them.
40
+ * A tool a plugin registers with a non-activity venue is not listed, so the daemon may count it
41
+ * (a headline can appear on a turn whose bar shows fewer calls than the cutoff only in that
42
+ * corner, and the renderer re-checks its own count before displaying anything).
43
+ */
44
+ export declare const NON_ACTIVITY_TOOL_NAMES: readonly string[];
45
+
46
+ /**
47
+ * The daemon's own run-start record. A run's entries begin here, before the
48
+ * user message that opened it.
49
+ */
50
+ export declare const RUN_STARTED_CUSTOM_TYPE = "session_run_started";
51
+
52
+ /**
53
+ * The daemon's own terminal run record (completed / interrupted / cancelled /
54
+ * failed). A run's entries end here.
55
+ */
56
+ export declare const RUN_TERMINAL_CUSTOM_TYPE = "session_run_state";
57
+
31
58
  /** The fields the customType selector needs off a session entry. */
32
59
  export declare interface TranscriptEntryLike {
33
60
  readonly id: string;
@@ -57,4 +84,86 @@ declare interface TranscriptTreeEntryLike extends TranscriptEntryLike {
57
84
  readonly targetId?: string | null;
58
85
  }
59
86
 
87
+ /**
88
+ * The fields the turn walk reads. Deliberately narrower and looser than
89
+ * {@link TranscriptEntryLike}: the walk answers in INDEXES, so it never needs an
90
+ * `id`, and both callers hold entries typed by their own layer (herd's
91
+ * `TranscriptEntry`, the daemon's branch rows).
92
+ */
93
+ export declare interface TurnBoundaryEntry {
94
+ readonly type?: string;
95
+ readonly customType?: string;
96
+ /** Opaque here; only `message.role === "user"` is read, defensively. */
97
+ readonly message?: unknown;
98
+ }
99
+
100
+ /** One turn's bounds as {@link turnSegments} reports them, both inclusive. */
101
+ export declare interface TurnSegment {
102
+ /** The user message entry that opened the turn. A steer never opens one. */
103
+ readonly openingIndex: number;
104
+ /**
105
+ * The entry the turn settled on. Equal to `openingIndex` when nothing
106
+ * followed the opening message, which is a turn that never settled rather
107
+ * than a turn with an empty body.
108
+ */
109
+ readonly settleIndex: number;
110
+ }
111
+
112
+ /**
113
+ * THE definition of a turn, shared by every reader that has to agree about one:
114
+ * the settled-turn extractors that key a record to a turn, and the replay
115
+ * publisher that offers a stored session's turns as events. Two readers
116
+ * disagreeing here means a replayed "turn" is not the turn production would
117
+ * have extracted.
118
+ *
119
+ * **A turn is a run.** The daemon brackets every run in the transcript
120
+ * ({@link RUN_STARTED_CUSTOM_TYPE} then, at the end,
121
+ * {@link RUN_TERMINAL_CUSTOM_TYPE}), and the user message that opened the run
122
+ * opens the turn. A human steering or a queued message admitted mid-flight is
123
+ * persisted as another `role: "user"` entry INSIDE that bracket, and belongs to
124
+ * the turn it interrupted: it is a correction to work already under way, not a
125
+ * new request. Anchoring on the last user entry instead hands an extractor the
126
+ * steer as the turn's goal and demotes the real request to prior context.
127
+ *
128
+ * A transcript with no run brackets at all (a log older than run bookkeeping, a
129
+ * hand-built fixture) falls back to one turn per user message, which is the
130
+ * most this function can honestly say: with no record of when a run ended,
131
+ * nothing distinguishes a steer from a fresh request.
132
+ *
133
+ * `throughIndex` bounds the walk for a caller holding only a prefix; segments
134
+ * are returned oldest first.
135
+ */
136
+ export declare function turnSegments(entries: readonly TurnBoundaryEntry[], throughIndex?: number): readonly TurnSegment[];
137
+
138
+ export declare const WORK_HEADLINE_MAX_CHARS = 90;
139
+
140
+ /** The longest and shortest stored work headline, in characters. Shorter is filler; longer is a sentence. */
141
+ export declare const WORK_HEADLINE_MIN_CHARS = 12;
142
+
143
+ /**
144
+ * The fewest VISIBLE tool calls a settled turn needs before it earns a work headline (owner
145
+ * ruling, 2026-10-14: "it shouldn't do that if it ran 2 commands but could be helpful if it ran
146
+ * like 15"; the scope card's 10 was accepted). ONE value, read by the writer (the hindsight
147
+ * `work-headline` handler decides whether to spend a model call) and by the closed work bar's
148
+ * renderer (decides whether to show a stored headline), so the two can never disagree about which
149
+ * turns qualify.
150
+ */
151
+ export declare const WORK_HEADLINE_MIN_VISIBLE_CALLS = 10;
152
+
153
+ export declare type WorkBlockCoverage = z.infer<typeof WorkBlockCoverageSchema>;
154
+
155
+ export declare function workBlockCoverage(toolResultEntryIds: readonly string[]): WorkBlockCoverage | undefined;
156
+
157
+ /**
158
+ * Exact ordered completed executions, addressed by persisted tool-result entry IDs.
159
+ * Provider call IDs can repeat across retries and turns; they are never ownership keys.
160
+ */
161
+ export declare const WorkBlockCoverageSchema: z.ZodObject<{
162
+ firstToolResultEntryId: z.ZodString;
163
+ lastToolResultEntryId: z.ZodString;
164
+ toolResultEntryIds: z.ZodArray<z.ZodString>;
165
+ }, z.core.$strict>;
166
+
167
+ export declare function workBlockKey(coverage: WorkBlockCoverage): string;
168
+
60
169
  export { }
@@ -5,6 +5,22 @@ var WorkBlockCoverageSchema = z.strictObject({
5
5
  lastToolResultEntryId: z.string().min(1),
6
6
  toolResultEntryIds: z.array(z.string().min(1)).min(1).max(4096)
7
7
  }).refine((value) => value.firstToolResultEntryId === value.toolResultEntryIds[0] && value.lastToolResultEntryId === value.toolResultEntryIds.at(-1) && new Set(value.toolResultEntryIds).size === value.toolResultEntryIds.length);
8
+ function workBlockCoverage(toolResultEntryIds) {
9
+ const parsed = WorkBlockCoverageSchema.safeParse({
10
+ firstToolResultEntryId: toolResultEntryIds[0],
11
+ lastToolResultEntryId: toolResultEntryIds.at(-1),
12
+ toolResultEntryIds
13
+ });
14
+ return parsed.success ? parsed.data : void 0;
15
+ }
16
+ function workBlockKey(coverage) {
17
+ return JSON.stringify(coverage.toolResultEntryIds);
18
+ }
19
+ function matchesWorkBlock(coverage, toolResultEntryIds) {
20
+ return coverage.toolResultEntryIds.length === toolResultEntryIds.length && coverage.toolResultEntryIds.every((id, index) => id === toolResultEntryIds[index]);
21
+ }
22
+ var RUN_STARTED_CUSTOM_TYPE = "session_run_started";
23
+ var RUN_TERMINAL_CUSTOM_TYPE = "session_run_state";
8
24
  function branchEntryIds(entries) {
9
25
  const last = entries[entries.length - 1];
10
26
  if (last === void 0)
@@ -19,11 +35,77 @@ function branchEntryIds(entries) {
19
35
  return onBranch;
20
36
  }
21
37
  var EMPTY_BRANCH_IDS = /* @__PURE__ */ new Set();
38
+ function isUserMessageEntry(entry) {
39
+ if (entry.type !== "message")
40
+ return false;
41
+ const message = entry.message;
42
+ return typeof message === "object" && message !== null && message.role === "user";
43
+ }
44
+ function runBoundaryOf(entry) {
45
+ if (entry.type !== "custom")
46
+ return void 0;
47
+ if (entry.customType === RUN_STARTED_CUSTOM_TYPE)
48
+ return "started";
49
+ if (entry.customType === RUN_TERMINAL_CUSTOM_TYPE)
50
+ return "terminal";
51
+ return void 0;
52
+ }
53
+ function turnSegments(entries, throughIndex = entries.length - 1) {
54
+ const segments = [];
55
+ let openingIndex;
56
+ let runOpen = false;
57
+ const close = (settleIndex) => {
58
+ if (openingIndex !== void 0 && settleIndex >= openingIndex) {
59
+ segments.push({ openingIndex, settleIndex });
60
+ }
61
+ openingIndex = void 0;
62
+ };
63
+ for (let index = 0; index <= throughIndex; index += 1) {
64
+ const entry = entries[index];
65
+ if (entry === void 0)
66
+ break;
67
+ const boundary = runBoundaryOf(entry);
68
+ if (boundary !== void 0) {
69
+ close(boundary === "terminal" ? index : index - 1);
70
+ runOpen = boundary === "started";
71
+ continue;
72
+ }
73
+ if (!isUserMessageEntry(entry))
74
+ continue;
75
+ if (runOpen && openingIndex !== void 0)
76
+ continue;
77
+ close(index - 1);
78
+ openingIndex = index;
79
+ }
80
+ close(throughIndex);
81
+ return segments;
82
+ }
22
83
  function customEntriesFromEntries(entries, customType, options) {
23
84
  const onBranch = options.branch === "all" ? null : branchEntryIds(entries);
24
85
  return entries.filter((entry) => entry.type === "custom" && entry.customType === customType && (onBranch === null || onBranch.has(entry.id)));
25
86
  }
87
+ var WORK_HEADLINE_MIN_VISIBLE_CALLS = 10;
88
+ var NON_ACTIVITY_TOOL_NAMES = [
89
+ "agent_spawn",
90
+ "send_message",
91
+ "service_connect",
92
+ "ui_render",
93
+ "submit_final"
94
+ ];
95
+ var WORK_HEADLINE_MIN_CHARS = 12;
96
+ var WORK_HEADLINE_MAX_CHARS = 90;
26
97
  export {
98
+ NON_ACTIVITY_TOOL_NAMES,
99
+ RUN_STARTED_CUSTOM_TYPE,
100
+ RUN_TERMINAL_CUSTOM_TYPE,
101
+ WORK_HEADLINE_MAX_CHARS,
102
+ WORK_HEADLINE_MIN_CHARS,
103
+ WORK_HEADLINE_MIN_VISIBLE_CALLS,
104
+ WorkBlockCoverageSchema,
27
105
  branchEntryIds,
28
- customEntriesFromEntries
106
+ customEntriesFromEntries,
107
+ matchesWorkBlock,
108
+ turnSegments,
109
+ workBlockCoverage,
110
+ workBlockKey
29
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaklabs-ai/plugin-sdk",
3
- "version": "0.13.5",
3
+ "version": "0.13.6",
4
4
  "description": "The Kay plugin SDK — types and helpers for authoring a Kay directory plugin.",
5
5
  "author": "YakLabs",
6
6
  "type": "module",
@@ -44,6 +44,10 @@
44
44
  "./transcript": {
45
45
  "types": "./dist/transcript.d.ts",
46
46
  "default": "./dist/transcript.js"
47
+ },
48
+ "./time": {
49
+ "types": "./dist/time.d.ts",
50
+ "default": "./dist/time.js"
47
51
  }
48
52
  },
49
53
  "files": [
@@ -56,7 +60,8 @@
56
60
  ],
57
61
  "dependencies": {
58
62
  "zod": "^4.0.0",
59
- "yaml": "^2.9.0"
63
+ "yaml": "^2.9.0",
64
+ "@js-temporal/polyfill": "0.5.1"
60
65
  },
61
66
  "publishConfig": {
62
67
  "registry": "https://registry.npmjs.org",
@@ -6,7 +6,7 @@ description: "Author, scaffold, debug, or iterate on a Bonsai directory plugin (
6
6
  # Directory plugin authoring
7
7
 
8
8
  A directory plugin is a plugin the user authors in `~/.kay/plugins/<name>/`, outside the Bonsai
9
- repo. The full authoring guide is [`docs/directory-plugins.md`](../../docs/directory-plugins.md)
9
+ repo. The full authoring guide is [`docs/directory-plugins.md`](../../../docs/directory-plugins.md)
10
10
  and the SDK reference is `packages/plugin-sdk/README.md`; this skill is the routing layer that
11
11
  keeps an agent on the paved path and out of the known pits.
12
12
 
@@ -85,17 +85,17 @@ separates:
85
85
  `<namespace>.<localId>` space ACROSS kinds, so a tool and an endpoint both called `inspect`
86
86
  collide and the whole plugin is refused (#8936).
87
87
  - **Loads, minus something** — a retired `compatibility` / `permissions.byTarget` key the parser
88
- strips silently; plus compatibility notes (no declared SDK range).
88
+ strips silently, or a tool whose `toolName` is not the namespace-derived name (that tool is left
89
+ out); plus compatibility notes (no declared SDK range).
89
90
 
90
91
  It never reads the declared files or applies the roster's rules: a missing entrypoint or skill
91
- file, a `toolName` that is not the namespace-derived name, an `activate()` that throws, the
92
- disclosure audit, and an id another installed plugin already claims all show only after enabling,
93
- in `read plugin://<id>`.
92
+ file, an `activate()` that throws, the disclosure audit, and an id another installed plugin already
93
+ claims all show only after enabling, in `read plugin://<id>`.
94
94
 
95
95
  ## The SDK resolution contract
96
96
 
97
97
  - Plugin source imports `@bonsai/plugin-sdk/<subpath>` as a **bare specifier**
98
- (`@bonsai/plugin-sdk/kay-agent`, `/kay-agent/testing`, `/kay-app`, `/disclosure` — only declared
98
+ (`@bonsai/plugin-sdk/kay-agent`, `/kay-agent/testing`, `/kay-app`, `/disclosure`, `/time` — only declared
99
99
  exports exist).
100
100
  - The daemon's plugin loader bundles each declared TypeScript entrypoint and its relative imports.
101
101
  It keeps `@bonsai/plugin-sdk` imports external and resolves them to the running app's own built SDK.
@@ -293,7 +293,7 @@ file.
293
293
 
294
294
  ## Reference map
295
295
 
296
- - [`docs/directory-plugins.md`](../../docs/directory-plugins.md) — the authoring guide:
296
+ - [`docs/directory-plugins.md`](../../../docs/directory-plugins.md) — the authoring guide:
297
297
  manifest shape, the enable gate and what it does not gate, trust tiers, diagnostics.
298
298
  - `packages/plugin-sdk/README.md` — the SDK surface.
299
299
  - `packages/plugin-sdk/PUBLISHED.md` — installing the published SDK outside a Bonsai checkout.