@directive-run/claude-plugin 1.18.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/claude-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
4
4
|
"description": "Claude Code plugin for Directive — 12 skills covering modules, constraints, resolvers, derivations, AI orchestration, and adapters. Installable via Claude Code's plugin marketplace or consumable programmatically as an npm package.",
|
|
5
5
|
"license": "(MIT OR Apache-2.0)",
|
|
6
6
|
"author": "Jason Comes",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"tsx": "^4.19.2",
|
|
53
53
|
"typescript": "^5.7.2",
|
|
54
54
|
"vitest": "^3.0.0",
|
|
55
|
-
"@directive-run/knowledge": "1.
|
|
55
|
+
"@directive-run/knowledge": "1.19.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsx scripts/build-skills.ts && tsup",
|
|
@@ -54,7 +54,11 @@ gate execution on `facts.<lifecycle>.healthy` derivations.
|
|
|
54
54
|
- **MCP server connect/disconnect** → see "MCP Lifecycle" below.
|
|
55
55
|
- **Health checks** → poll inside the source's `attach`; publish
|
|
56
56
|
`health_changed` events as the upstream service responds.
|
|
57
|
-
- **WebSocket open / close** → `
|
|
57
|
+
- **WebSocket open / close (Cloudflare DO)** → `sourceFromWebSocketMessage()`
|
|
58
|
+
(`@directive-run/sources/cloudflare`). For raw Node/browser WebSocket
|
|
59
|
+
bridges, declare a source whose `attach` wires `addEventListener` and
|
|
60
|
+
publishes `MESSAGE` / `CLOSE` / `ERROR` events (a generic
|
|
61
|
+
`sourceFromWebSocket()` helper is queued for a follow-up RFC).
|
|
58
62
|
|
|
59
63
|
### "I want my agent's facts to update from a live external feed"
|
|
60
64
|
|
|
@@ -101,9 +105,6 @@ const result = orchestrator.runStream(agent, input, {
|
|
|
101
105
|
system, // the Directive system whose facts feed the agent
|
|
102
106
|
keys: ["pr.headSha", "pr.state"], // REQUIRED — fact keys to watch
|
|
103
107
|
interruptWhen: (facts, changedKeys) => boolean, // optional; default: () => true
|
|
104
|
-
mode: "inject-system-message", // shipped today (default behavior).
|
|
105
|
-
// "restart" reserved for follow-up minor;
|
|
106
|
-
// behaves identically to inject-system-message today.
|
|
107
108
|
notifyOn: "interrupt-only", // default; "all-changes" emits chunk per watched change
|
|
108
109
|
onContextUpdate: (changedKeys) => void, // optional logging hook
|
|
109
110
|
},
|
|
@@ -154,10 +155,14 @@ Two new chunk variants land on the stream:
|
|
|
154
155
|
`runStream({ liveContext })` is the dedicated subject of
|
|
155
156
|
[RFC 0005](../../docs/rfcs/0005-live-context-agent.md). Today's
|
|
156
157
|
implementation is **231 LOC in `agent-orchestrator.ts`** — under the
|
|
157
|
-
RFC's 300-LOC scope guard.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
RFC's 300-LOC scope guard. Behavior is **abort-and-emit**: when
|
|
159
|
+
`interruptWhen` returns `true`, the orchestrator aborts the in-flight
|
|
160
|
+
LLM run and emits an `interrupted` chunk with the partial output; the
|
|
161
|
+
caller resumes by issuing a fresh `runStream` against the still-live
|
|
162
|
+
subscription (or fully tears down via `result.abort()`). Automatic
|
|
163
|
+
re-invocation (the "restart" semantic the original RFC drafted) is
|
|
164
|
+
reserved for a follow-up RFC + field — the original `mode` field was
|
|
165
|
+
removed before release because the impl never read it.
|
|
161
166
|
|
|
162
167
|
**Security companion (mandatory when watched facts may carry PII):**
|
|
163
168
|
wire `createFactPIIGuardrail` on the same Directive system. Without
|
|
@@ -170,70 +175,118 @@ threat model.
|
|
|
170
175
|
|
|
171
176
|
## MCP Lifecycle as a Source
|
|
172
177
|
|
|
173
|
-
`@directive-run/ai/mcp`
|
|
174
|
-
(`onConnect`, `onDisconnect`, `onToolCall`, ...) as
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
`@directive-run/ai/mcp` exposes `MCPAdapterConfig.events`
|
|
179
|
+
(`onConnect`, `onDisconnect`, `onToolCall`, `onError`, ...) as a single
|
|
180
|
+
bag of callbacks set at adapter-construction time. To bridge the
|
|
181
|
+
adapter's lifecycle into a Directive source the recipe is: declare a
|
|
182
|
+
holder that the source's `attach` populates with the live `publish`,
|
|
183
|
+
then point the adapter's `events.onConnect` / `onDisconnect` at the
|
|
184
|
+
holder. The holder pattern lets you construct the adapter and the
|
|
185
|
+
source in either order — the source's mount/unmount drives which
|
|
186
|
+
publish closure (if any) receives lifecycle events.
|
|
187
|
+
|
|
188
|
+
> **Multi-tenant safety.** If you import the module that owns the
|
|
189
|
+
> holder from two places (e.g., a Worker that mounts one Directive
|
|
190
|
+
> system per tenant DO), a module-level `let publishRef` would be
|
|
191
|
+
> SHARED — the second tenant's `attach` would silently overwrite the
|
|
192
|
+
> first tenant's pipe. **Always declare the holder + adapter inside a
|
|
193
|
+
> factory function** so each call yields an isolated closure pair.
|
|
194
|
+
> The recipe below uses `makeOrchestrator(adapter)` for exactly this
|
|
195
|
+
> reason; if you create the adapter outside the factory, pass it in
|
|
196
|
+
> per call too.
|
|
177
197
|
|
|
178
198
|
```ts
|
|
179
199
|
import { createSystem, createModule, t } from "@directive-run/core";
|
|
200
|
+
import type { SourcePublish } from "@directive-run/core";
|
|
180
201
|
import { createMCPAdapter } from "@directive-run/ai/mcp";
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
// Factory closure — each call yields a fresh `(adapter, module)`
|
|
204
|
+
// pair with its own `publishRef`. Multi-tenant safe; SSR / Vitest
|
|
205
|
+
// hot-reload safe; one tenant's MCP traffic can never leak into
|
|
206
|
+
// another tenant's facts.
|
|
207
|
+
function makeOrchestrator() {
|
|
208
|
+
let publishRef: SourcePublish | null = null;
|
|
183
209
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
facts: {
|
|
187
|
-
mcp: t.object<{ servers: Record<string, "connected" | "disconnected"> }>(),
|
|
188
|
-
},
|
|
210
|
+
const adapter = createMCPAdapter({
|
|
211
|
+
servers: [/* ... */],
|
|
189
212
|
events: {
|
|
190
|
-
|
|
191
|
-
|
|
213
|
+
onConnect: (name) => publishRef?.("MCP_SERVER_CONNECTED", { name }),
|
|
214
|
+
onDisconnect: (name) =>
|
|
215
|
+
publishRef?.("MCP_SERVER_DISCONNECTED", { name }),
|
|
192
216
|
},
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const orchestrator = createModule("orchestrator", {
|
|
220
|
+
schema: {
|
|
221
|
+
facts: {
|
|
222
|
+
mcp: t.object<{
|
|
223
|
+
servers: Record<string, "connected" | "disconnected">;
|
|
224
|
+
}>(),
|
|
225
|
+
},
|
|
226
|
+
events: {
|
|
227
|
+
MCP_SERVER_CONNECTED: { name: t.string() },
|
|
228
|
+
MCP_SERVER_DISCONNECTED: { name: t.string() },
|
|
229
|
+
},
|
|
230
|
+
derivations: {
|
|
231
|
+
allServersHealthy: t.boolean(),
|
|
232
|
+
},
|
|
203
233
|
},
|
|
204
|
-
|
|
205
|
-
f.mcp = {
|
|
206
|
-
...f.mcp,
|
|
207
|
-
servers: { ...f.mcp.servers, [p.name]: "disconnected" },
|
|
208
|
-
};
|
|
234
|
+
init: (f) => {
|
|
235
|
+
f.mcp = { servers: {} };
|
|
209
236
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const off2 = adapter.onDisconnect((name) =>
|
|
222
|
-
publish("MCP_SERVER_DISCONNECTED", { name }),
|
|
223
|
-
);
|
|
224
|
-
return () => {
|
|
225
|
-
off1();
|
|
226
|
-
off2();
|
|
237
|
+
events: {
|
|
238
|
+
MCP_SERVER_CONNECTED: (f, p) => {
|
|
239
|
+
f.mcp = {
|
|
240
|
+
...f.mcp,
|
|
241
|
+
servers: { ...f.mcp.servers, [p.name]: "connected" },
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
MCP_SERVER_DISCONNECTED: (f, p) => {
|
|
245
|
+
f.mcp = {
|
|
246
|
+
...f.mcp,
|
|
247
|
+
servers: { ...f.mcp.servers, [p.name]: "disconnected" },
|
|
227
248
|
};
|
|
228
249
|
},
|
|
229
250
|
},
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
derive: {
|
|
252
|
+
allServersHealthy: (facts) =>
|
|
253
|
+
Object.values(facts.mcp.servers).every((s) => s === "connected"),
|
|
254
|
+
},
|
|
255
|
+
sources: {
|
|
256
|
+
mcpLifecycle: {
|
|
257
|
+
attach: (publish) => {
|
|
258
|
+
publishRef = publish;
|
|
259
|
+
// Kick the adapter's connection lifecycle once the source is
|
|
260
|
+
// attached so the first `onConnect` flows into our publish.
|
|
261
|
+
void adapter.connect();
|
|
262
|
+
return async () => {
|
|
263
|
+
publishRef = null;
|
|
264
|
+
await adapter.disconnect();
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return { orchestrator, adapter };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Per-tenant / per-request usage:
|
|
275
|
+
const { orchestrator, adapter } = makeOrchestrator();
|
|
276
|
+
const system = createSystem({ module: orchestrator });
|
|
232
277
|
```
|
|
233
278
|
|
|
234
279
|
Constraints can now gate agent execution on
|
|
235
280
|
`facts.allServersHealthy` — no separate health-check loop needed.
|
|
236
281
|
|
|
282
|
+
> The holder + closure pattern is the canonical bridge from any
|
|
283
|
+
> single-callback-bag third-party SDK (MCP, telemetry SDKs that take an
|
|
284
|
+
> `onEvent` config at construction) into a Directive source. The
|
|
285
|
+
> alternative — passing `publish` from inside `attach` into the
|
|
286
|
+
> adapter's events — requires the adapter to be constructed AFTER the
|
|
287
|
+
> source is created, which is awkward when the adapter is used outside
|
|
288
|
+
> Directive too.
|
|
289
|
+
|
|
237
290
|
---
|
|
238
291
|
|
|
239
292
|
## Sources × Security
|
|
@@ -317,7 +370,8 @@ exports. One install, optional peerDependencies per vendor.
|
|
|
317
370
|
|---|---|---|
|
|
318
371
|
| `@directive-run/sources/supabase` | `sourceFromSupabaseChannel()` | Supabase realtime channel |
|
|
319
372
|
| `@directive-run/sources/cloudflare` | `sourceFromDOAlarm()` | Cloudflare Durable Object alarm |
|
|
320
|
-
| `@directive-run/sources/
|
|
373
|
+
| `@directive-run/sources/cloudflare` | `sourceFromWebSocketMessage()` | Cloudflare Durable Object WebSocket message stream |
|
|
374
|
+
| `@directive-run/sources/websocket` | `sourceFromWebSocket()` | (future RFC) raw browser / Node WebSocket |
|
|
321
375
|
| `@directive-run/sources/sentry` | `sourceFromSentryHook()` | (future RFC) Sentry production-error stream |
|
|
322
376
|
|
|
323
377
|
Install only the umbrella; the vendor peerDeps are optional and pull in
|
|
@@ -274,20 +274,38 @@ flushes return Promises) work with both sync and async teardown:
|
|
|
274
274
|
wall-clock cutoff so the runtime can evict the isolate even if
|
|
275
275
|
some sources hang.
|
|
276
276
|
|
|
277
|
+
Use the **holder + closure** bridge pattern (the same shape the MCP
|
|
278
|
+
recipe in `ai-sources.md` documents) so the `attach` and `onEvict`
|
|
279
|
+
sibling closures share the channel handle:
|
|
280
|
+
|
|
277
281
|
```typescript
|
|
282
|
+
// Holder shared between `attach` and `onEvict`. Both closures live
|
|
283
|
+
// inside the same `sources` object; declaring the channel above them
|
|
284
|
+
// keeps the handle reachable from both.
|
|
285
|
+
let channel: RealtimeChannel | null = null;
|
|
286
|
+
|
|
278
287
|
sources: {
|
|
279
288
|
channel: {
|
|
280
289
|
attach: (publish) => {
|
|
281
|
-
|
|
282
|
-
|
|
290
|
+
channel = supabase.channel("game");
|
|
291
|
+
channel.on("postgres_changes", { event: "UPDATE" }, (p) =>
|
|
283
292
|
publish("ROW_UPDATED", p),
|
|
284
293
|
);
|
|
285
|
-
|
|
294
|
+
channel.subscribe();
|
|
295
|
+
// Per RFC 0009, the unsubscribe MAY return a Promise; awaiting
|
|
296
|
+
// it on `system.stopAsync()` ensures the broker has dropped the
|
|
297
|
+
// subscription before the next start cycle re-attaches.
|
|
298
|
+
return async () => {
|
|
299
|
+
if (channel) await supabase.removeChannel(channel);
|
|
300
|
+
channel = null;
|
|
301
|
+
};
|
|
286
302
|
},
|
|
287
303
|
onEvict: async () => {
|
|
288
304
|
// Cloudflare DO hibernate signal: close the channel actively
|
|
289
305
|
// so the broker drops the subscription before the isolate dies.
|
|
290
|
-
|
|
306
|
+
// The unsubscribe will also run, but onEvict fires first and
|
|
307
|
+
// bounds the pre-hibernation work to a deadline.
|
|
308
|
+
if (channel) await supabase.removeChannel(channel);
|
|
291
309
|
},
|
|
292
310
|
},
|
|
293
311
|
}
|
|
@@ -421,4 +439,4 @@ sources: {
|
|
|
421
439
|
- `anti-patterns.md` (#20) — "subscribe inside an effect / useEffect" anti-pattern; prefer sources.
|
|
422
440
|
- `naming.md` — canonical-term entry for `sources` + cross-paradigm aliases (RxJS Observable, DOM EventTarget, XState callback actor, Supabase realtime).
|
|
423
441
|
- [`../ai/ai-sources.md`](../ai/ai-sources.md) — AI integration: `runStream({ liveContext })`, MCP lifecycle as a source, the `@directive-run/sources/*` adapter subpaths.
|
|
424
|
-
- [`../ai/ai-security.md`](../ai/ai-security.md#sources
|
|
442
|
+
- [`../ai/ai-security.md`](../ai/ai-security.md#sources--pii--closing-the-fact-injection-bypass) — `createFactPIIGuardrail`: closes the source → fact → agent-prompt PII bypass. Wire whenever sources publish into facts the agent reads.
|