@lumenflow/cli 3.9.5 → 3.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/cli",
3
- "version": "3.9.5",
3
+ "version": "3.9.6",
4
4
  "description": "Command-line interface for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -180,13 +180,13 @@
180
180
  "xstate": "^5.28.0",
181
181
  "yaml": "^2.8.2",
182
182
  "zod": "^4.3.6",
183
- "@lumenflow/agent": "3.9.5",
184
- "@lumenflow/initiatives": "3.9.5",
185
- "@lumenflow/kernel": "3.9.5",
186
- "@lumenflow/memory": "3.9.5",
187
- "@lumenflow/control-plane-sdk": "3.9.5",
188
- "@lumenflow/metrics": "3.9.5",
189
- "@lumenflow/core": "3.9.5"
183
+ "@lumenflow/agent": "3.9.6",
184
+ "@lumenflow/core": "3.9.6",
185
+ "@lumenflow/control-plane-sdk": "3.9.6",
186
+ "@lumenflow/initiatives": "3.9.6",
187
+ "@lumenflow/memory": "3.9.6",
188
+ "@lumenflow/kernel": "3.9.6",
189
+ "@lumenflow/metrics": "3.9.6"
190
190
  },
191
191
  "devDependencies": {
192
192
  "@vitest/coverage-v8": "^4.0.18",
@@ -1,4 +1,4 @@
1
1
 
2
- > @lumenflow/packs-sidekick@3.9.4 build /home/tom/source/hellmai/lumenflow-dev/packages/@lumenflow/packs/sidekick
2
+ > @lumenflow/packs-sidekick@3.9.6 build /home/runner/work/lumenflow-dev/lumenflow-dev/packages/@lumenflow/packs/sidekick
3
3
  > tsc
4
4
 
@@ -182,9 +182,11 @@ const TOOL_INPUT_SCHEMAS: Record<SidekickToolName, Record<string, unknown>> = {
182
182
  'channel:send': {
183
183
  type: 'object',
184
184
  properties: {
185
+ provider: { type: 'string' },
185
186
  channel: { type: 'string' },
186
187
  content: { type: 'string', minLength: 1 },
187
188
  sender: { type: 'string' },
189
+ metadata: { type: 'object', additionalProperties: true },
188
190
  dry_run: { type: 'boolean' },
189
191
  },
190
192
  required: ['content'],
@@ -193,9 +195,12 @@ const TOOL_INPUT_SCHEMAS: Record<SidekickToolName, Record<string, unknown>> = {
193
195
  'channel:receive': {
194
196
  type: 'object',
195
197
  properties: {
198
+ provider: { type: 'string' },
196
199
  channel: { type: 'string' },
200
+ cursor: { type: 'string' },
197
201
  limit: { type: 'integer', minimum: 1 },
198
202
  since: { type: 'string' },
203
+ metadata: { type: 'object', additionalProperties: true },
199
204
  },
200
205
  additionalProperties: false,
201
206
  },
@@ -235,6 +235,8 @@ tools:
235
235
  input_schema:
236
236
  type: object
237
237
  properties:
238
+ provider:
239
+ type: string
238
240
  channel:
239
241
  type: string
240
242
  content:
@@ -242,6 +244,9 @@ tools:
242
244
  minLength: 1
243
245
  sender:
244
246
  type: string
247
+ metadata:
248
+ type: object
249
+ additionalProperties: true
245
250
  dry_run:
246
251
  type: boolean
247
252
  required: [content]
@@ -259,13 +264,20 @@ tools:
259
264
  input_schema:
260
265
  type: object
261
266
  properties:
267
+ provider:
268
+ type: string
262
269
  channel:
263
270
  type: string
271
+ cursor:
272
+ type: string
264
273
  limit:
265
274
  type: integer
266
275
  minimum: 1
267
276
  since:
268
277
  type: string
278
+ metadata:
279
+ type: object
280
+ additionalProperties: true
269
281
  additionalProperties: false
270
282
  output_schema:
271
283
  type: object
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/packs-sidekick",
3
- "version": "3.9.5",
3
+ "version": "3.9.6",
4
4
  "description": "Sidekick personal assistant pack for LumenFlow — 16 tools for task management, typed memory, channels, routines, and audit",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -27,6 +27,7 @@
27
27
  "./storage": "./dist/tool-impl/storage.js",
28
28
  "./tools": "./dist/tools/index.js",
29
29
  "./tool-impl": "./dist/tool-impl/index.js",
30
+ "./tool-impl/channel-transports": "./dist/tool-impl/channel-transports.js",
30
31
  "./tool-impl/channel-tools": "./dist/tool-impl/channel-tools.js",
31
32
  "./tool-impl/memory-tools": "./dist/tool-impl/memory-tools.js",
32
33
  "./tool-impl/routine-tools": "./dist/tool-impl/routine-tools.js",
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2026 Hellmai Ltd
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
 
4
+ import { getChannelTransport } from './channel-transports.js';
4
5
  import { getStoragePort, type ChannelMessageRecord, type ChannelRecord } from './storage.js';
5
6
  import {
6
7
  asInteger,
@@ -29,6 +30,26 @@ const TOOL_NAMES = {
29
30
  const OUTBOX_CAP = 100;
30
31
  const DEFAULT_CHANNEL_NAME = 'default';
31
32
 
33
+ function asOptionalRecord(value: unknown): Record<string, unknown> | undefined {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
35
+ return undefined;
36
+ }
37
+ return value as Record<string, unknown>;
38
+ }
39
+
40
+ function withTransportMetadata<T extends Record<string, unknown>>(
41
+ data: T,
42
+ metadata: Record<string, unknown> | undefined,
43
+ ): T & Record<string, unknown> {
44
+ if (!metadata) {
45
+ return data;
46
+ }
47
+ return {
48
+ ...data,
49
+ metadata,
50
+ };
51
+ }
52
+
32
53
  // ---------------------------------------------------------------------------
33
54
  // channel:configure
34
55
  // ---------------------------------------------------------------------------
@@ -99,6 +120,77 @@ async function channelConfigureTool(
99
120
  // channel:send
100
121
  // ---------------------------------------------------------------------------
101
122
 
123
+ async function channelSendViaTransport(
124
+ input: Record<string, unknown>,
125
+ context: ToolContextLike | undefined,
126
+ provider: string,
127
+ channel: string,
128
+ content: string,
129
+ ): Promise<ToolOutput> {
130
+ const workspaceId = asNonEmptyString(context?.workspace_id);
131
+ if (!workspaceId) {
132
+ return failure(
133
+ 'WORKSPACE_CONTEXT_REQUIRED',
134
+ 'workspace_id is required when provider is specified.',
135
+ );
136
+ }
137
+
138
+ const transport = getChannelTransport(provider);
139
+ if (!transport) {
140
+ return failure(
141
+ 'INTEGRATION_PROVIDER_NOT_REGISTERED',
142
+ `No channel transport registered for provider: ${provider}`,
143
+ );
144
+ }
145
+
146
+ if (isDryRun(input)) {
147
+ return success({
148
+ dry_run: true,
149
+ provider,
150
+ capability: 'send',
151
+ channel,
152
+ content,
153
+ });
154
+ }
155
+
156
+ const transportResult = await transport.send({
157
+ workspaceId,
158
+ provider,
159
+ channel,
160
+ content,
161
+ metadata: asOptionalRecord(input.metadata),
162
+ });
163
+
164
+ const outputData = withTransportMetadata(
165
+ {
166
+ provider,
167
+ capability: 'send',
168
+ channel,
169
+ ...(transportResult.externalMessageId !== undefined
170
+ ? { externalMessageId: transportResult.externalMessageId }
171
+ : {}),
172
+ ...(transportResult.failureClass ? { failureClass: transportResult.failureClass } : {}),
173
+ ...(transportResult.retryAfterSeconds !== undefined
174
+ ? { retryAfterSeconds: transportResult.retryAfterSeconds }
175
+ : {}),
176
+ },
177
+ transportResult.metadata,
178
+ );
179
+
180
+ if (!transportResult.success) {
181
+ return {
182
+ success: false,
183
+ error: {
184
+ code: 'INTEGRATION_PROVIDER_SEND_FAILED',
185
+ message: transportResult.error ?? `Provider send failed for provider: ${provider}`,
186
+ },
187
+ data: outputData,
188
+ };
189
+ }
190
+
191
+ return success(outputData);
192
+ }
193
+
102
194
  async function channelSendTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
103
195
  const parsed = toRecord(input);
104
196
  const content = asNonEmptyString(parsed.content);
@@ -108,6 +200,12 @@ async function channelSendTool(input: unknown, context?: ToolContextLike): Promi
108
200
  }
109
201
 
110
202
  const channelName = asNonEmptyString(parsed.channel) ?? DEFAULT_CHANNEL_NAME;
203
+ const provider = asNonEmptyString(parsed.provider);
204
+
205
+ if (provider) {
206
+ return channelSendViaTransport(parsed, context, provider, channelName, content);
207
+ }
208
+
111
209
  const sender = asNonEmptyString(parsed.sender) ?? 'assistant';
112
210
  const now = nowIso();
113
211
 
@@ -175,10 +273,86 @@ async function channelSendTool(input: unknown, context?: ToolContextLike): Promi
175
273
  // channel:receive
176
274
  // ---------------------------------------------------------------------------
177
275
 
276
+ async function channelReceiveViaTransport(
277
+ input: Record<string, unknown>,
278
+ context: ToolContextLike | undefined,
279
+ provider: string,
280
+ channel: string,
281
+ limit: number | null,
282
+ ): Promise<ToolOutput> {
283
+ const workspaceId = asNonEmptyString(context?.workspace_id);
284
+ if (!workspaceId) {
285
+ return failure(
286
+ 'WORKSPACE_CONTEXT_REQUIRED',
287
+ 'workspace_id is required when provider is specified.',
288
+ );
289
+ }
290
+
291
+ const transport = getChannelTransport(provider);
292
+ if (!transport) {
293
+ return failure(
294
+ 'INTEGRATION_PROVIDER_NOT_REGISTERED',
295
+ `No channel transport registered for provider: ${provider}`,
296
+ );
297
+ }
298
+
299
+ const cursor = asNonEmptyString(input.cursor) ?? undefined;
300
+
301
+ const transportResult = await transport.receive({
302
+ workspaceId,
303
+ provider,
304
+ channel,
305
+ cursor,
306
+ limit: limit && limit > 0 ? limit : undefined,
307
+ metadata: asOptionalRecord(input.metadata),
308
+ });
309
+
310
+ const outputData = withTransportMetadata(
311
+ {
312
+ provider,
313
+ capability: 'read',
314
+ channel,
315
+ ...(transportResult.records !== undefined ? { records: transportResult.records } : {}),
316
+ ...(transportResult.nextCursor !== undefined
317
+ ? { nextCursor: transportResult.nextCursor }
318
+ : {}),
319
+ ...(transportResult.failureClass ? { failureClass: transportResult.failureClass } : {}),
320
+ ...(transportResult.retryAfterSeconds !== undefined
321
+ ? { retryAfterSeconds: transportResult.retryAfterSeconds }
322
+ : {}),
323
+ },
324
+ transportResult.metadata,
325
+ );
326
+
327
+ if (!transportResult.success) {
328
+ return {
329
+ success: false,
330
+ error: {
331
+ code: 'INTEGRATION_PROVIDER_RECEIVE_FAILED',
332
+ message: transportResult.error ?? `Provider receive failed for provider: ${provider}`,
333
+ },
334
+ data: outputData,
335
+ };
336
+ }
337
+
338
+ return success(outputData);
339
+ }
340
+
178
341
  async function channelReceiveTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
179
342
  const parsed = toRecord(input);
180
343
  const channelName = asNonEmptyString(parsed.channel);
181
344
  const limit = asInteger(parsed.limit);
345
+ const provider = asNonEmptyString(parsed.provider);
346
+
347
+ if (provider) {
348
+ return channelReceiveViaTransport(
349
+ parsed,
350
+ _context,
351
+ provider,
352
+ channelName ?? DEFAULT_CHANNEL_NAME,
353
+ limit,
354
+ );
355
+ }
182
356
 
183
357
  const storage = getStoragePort();
184
358
  const messages = await storage.readStore('messages');
@@ -0,0 +1,75 @@
1
+ // Copyright (c) 2026 Hellmai Ltd
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ import { getSidekickRuntimeContext } from './runtime-context.js';
5
+
6
+ export interface ChannelTransport {
7
+ provider: string;
8
+ send(req: {
9
+ workspaceId: string;
10
+ provider: string;
11
+ channel: string;
12
+ content: string;
13
+ metadata?: Record<string, unknown>;
14
+ }): Promise<{
15
+ success: boolean;
16
+ error?: string;
17
+ externalMessageId?: string;
18
+ failureClass?: 'retryable' | 'terminal';
19
+ retryAfterSeconds?: number;
20
+ metadata?: Record<string, unknown>;
21
+ }>;
22
+ receive(req: {
23
+ workspaceId: string;
24
+ provider: string;
25
+ channel: string;
26
+ cursor?: string;
27
+ limit?: number;
28
+ metadata?: Record<string, unknown>;
29
+ }): Promise<{
30
+ success: boolean;
31
+ error?: string;
32
+ records?: unknown[];
33
+ nextCursor?: string;
34
+ failureClass?: 'retryable' | 'terminal';
35
+ retryAfterSeconds?: number;
36
+ metadata?: Record<string, unknown>;
37
+ }>;
38
+ }
39
+
40
+ function normalizeProvider(provider: string): string {
41
+ return provider.trim().toLowerCase();
42
+ }
43
+
44
+ function getRegistry(): Map<string, ChannelTransport> | undefined {
45
+ return getSidekickRuntimeContext()?.channelTransports;
46
+ }
47
+
48
+ export function registerChannelTransport(transport: ChannelTransport): void {
49
+ const provider = normalizeProvider(transport.provider);
50
+ if (provider.length === 0) {
51
+ throw new Error('channel transport provider must be a non-empty string.');
52
+ }
53
+ const registry = getRegistry();
54
+ if (!registry) {
55
+ throw new Error('channel transport registry is unavailable outside sidekick runtime context.');
56
+ }
57
+ registry.set(provider, transport);
58
+ }
59
+
60
+ export function getChannelTransport(provider: string): ChannelTransport | undefined {
61
+ const normalized = normalizeProvider(provider);
62
+ if (normalized.length === 0) {
63
+ return undefined;
64
+ }
65
+ const registry = getRegistry();
66
+ return registry?.get(normalized);
67
+ }
68
+
69
+ export function clearChannelTransports(): void {
70
+ const registry = getRegistry();
71
+ if (!registry) {
72
+ return;
73
+ }
74
+ registry.clear();
75
+ }
@@ -20,3 +20,10 @@ export {
20
20
  runWithStoragePort,
21
21
  setDefaultStoragePort,
22
22
  } from './storage.js';
23
+
24
+ export {
25
+ type ChannelTransport,
26
+ clearChannelTransports,
27
+ getChannelTransport,
28
+ registerChannelTransport,
29
+ } from './channel-transports.js';
@@ -0,0 +1,24 @@
1
+ // Copyright (c) 2026 Hellmai Ltd
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import type { ChannelTransport } from './channel-transports.js';
6
+ import type { StoragePort } from './storage.js';
7
+
8
+ export interface SidekickRuntimeContext {
9
+ storagePort: StoragePort;
10
+ channelTransports: Map<string, ChannelTransport>;
11
+ }
12
+
13
+ const runtimeContext = new AsyncLocalStorage<SidekickRuntimeContext>();
14
+
15
+ export function getSidekickRuntimeContext(): SidekickRuntimeContext | undefined {
16
+ return runtimeContext.getStore();
17
+ }
18
+
19
+ export async function runWithSidekickRuntimeContext<T>(
20
+ context: SidekickRuntimeContext,
21
+ fn: () => Promise<T>,
22
+ ): Promise<T> {
23
+ return runtimeContext.run(context, fn);
24
+ }
@@ -7,6 +7,7 @@ import type { AuditEvent } from './storage.js';
7
7
  export interface ToolContextLike {
8
8
  tool_name?: string;
9
9
  receipt_id?: string;
10
+ workspace_id?: string;
10
11
  }
11
12
 
12
13
  export interface ToolOutput {
@@ -1,10 +1,10 @@
1
1
  // Copyright (c) 2026 Hellmai Ltd
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
 
4
- import { AsyncLocalStorage } from 'node:async_hooks';
5
4
  import { randomBytes } from 'node:crypto';
6
5
  import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
7
6
  import path from 'node:path';
7
+ import { getSidekickRuntimeContext, runWithSidekickRuntimeContext } from './runtime-context.js';
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
10
  // Constants
@@ -299,7 +299,6 @@ export class FsStoragePort implements StoragePort {
299
299
  // Injection helpers (AsyncLocalStorage-based)
300
300
  // ---------------------------------------------------------------------------
301
301
 
302
- const storageContext = new AsyncLocalStorage<StoragePort>();
303
302
  let defaultStoragePort: StoragePort = new FsStoragePort();
304
303
 
305
304
  export function setDefaultStoragePort(port: StoragePort): void {
@@ -307,9 +306,16 @@ export function setDefaultStoragePort(port: StoragePort): void {
307
306
  }
308
307
 
309
308
  export function getStoragePort(): StoragePort {
310
- return storageContext.getStore() ?? defaultStoragePort;
309
+ return getSidekickRuntimeContext()?.storagePort ?? defaultStoragePort;
311
310
  }
312
311
 
313
312
  export async function runWithStoragePort<T>(port: StoragePort, fn: () => Promise<T>): Promise<T> {
314
- return storageContext.run(port, fn);
313
+ const existingContext = getSidekickRuntimeContext();
314
+ return runWithSidekickRuntimeContext(
315
+ {
316
+ storagePort: port,
317
+ channelTransports: existingContext?.channelTransports ?? new Map(),
318
+ },
319
+ fn,
320
+ );
315
321
  }
@@ -1,4 +1,4 @@
1
1
 
2
- > @lumenflow/packs-software-delivery@3.9.4 build /home/tom/source/hellmai/lumenflow-dev/packages/@lumenflow/packs/software-delivery
2
+ > @lumenflow/packs-software-delivery@3.9.6 build /home/runner/work/lumenflow-dev/lumenflow-dev/packages/@lumenflow/packs/software-delivery
3
3
  > tsc
4
4
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/packs-software-delivery",
3
- "version": "3.9.5",
3
+ "version": "3.9.6",
4
4
  "description": "Software delivery pack for LumenFlow — work units, gates, lanes, initiatives, and agent coordination",
5
5
  "keywords": [
6
6
  "lumenflow",