kanban-lite 1.2.2 → 1.2.4
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/cli.js +257 -90
- package/dist/extension.js +245 -78
- package/dist/mcp-server.js +117 -33
- package/dist/sdk/index.cjs +117 -32
- package/dist/sdk/index.mjs +117 -32
- package/dist/sdk/sdk/KanbanSDK.d.ts +27 -10
- package/dist/sdk/sdk/modules/cards.d.ts +11 -2
- package/dist/sdk/sdk/plugins/index.d.ts +7 -0
- package/dist/sdk/sdk/types.d.ts +12 -27
- package/dist/sdk/shared/config.d.ts +17 -1
- package/dist/standalone-webview/index.js +38 -38
- package/dist/standalone-webview/index.js.map +1 -1
- package/dist/standalone.js +307 -125
- package/package.json +1 -1
- package/src/cli/index.test.ts +157 -0
- package/src/cli/index.ts +1 -1
- package/src/mcp-server/index.test.ts +76 -0
- package/src/mcp-server/index.ts +1 -1
- package/src/sdk/KanbanSDK.d.ts +26 -10
- package/src/sdk/KanbanSDK.ts +37 -11
- package/src/sdk/__tests__/KanbanSDK.test.ts +79 -24
- package/src/sdk/integrationCatalog.ts +1 -0
- package/src/sdk/modules/cards.ts +13 -24
- package/src/sdk/plugins/index.d.ts +7 -0
- package/src/sdk/plugins/index.ts +17 -2
- package/src/sdk/types.d.ts +10 -26
- package/src/sdk/types.ts +11 -24
- package/src/sdk/webhooks.ts +19 -2
- package/src/shared/config.ts +130 -2
- package/src/standalone/__tests__/server.integration.test.ts +81 -2
- package/src/standalone/internal/runtime.ts +11 -6
- package/src/standalone/internal/websocket.ts +13 -3
- package/src/standalone/server.ts +67 -9
- package/src/standalone/watcherSetup.ts +9 -0
- package/src/webview/standalone-shim.ts +2 -1
- package/tmp/screenshots-workspace/.kanban/.active-card.json +5 -0
- package/tmp/screenshots-workspace/.kanban/boards/default/deleted/1-dddd.md +17 -0
- package/tmp/screenshots-workspace/.kanban/boards/default/deleted/attachments/1.log +1 -0
- package/tmp/screenshots-workspace/.kanban.json +59 -0
package/package.json
CHANGED
package/src/cli/index.test.ts
CHANGED
|
@@ -53,6 +53,85 @@ function createCliAuthWorkspace(): { workspaceDir: string; configPath: string; c
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function createCliWorkspace(config: Record<string, unknown>): { workspaceDir: string; configPath: string; cleanup: () => void } {
|
|
57
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kanban-cli-plugin-'))
|
|
58
|
+
fs.mkdirSync(path.join(workspaceDir, '.kanban'), { recursive: true })
|
|
59
|
+
const configPath = path.join(workspaceDir, '.kanban.json')
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
configPath,
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
version: 2,
|
|
64
|
+
defaultBoard: 'default',
|
|
65
|
+
kanbanDirectory: '.kanban',
|
|
66
|
+
boards: {
|
|
67
|
+
default: {
|
|
68
|
+
name: 'Default',
|
|
69
|
+
columns: [{ id: 'backlog', name: 'Backlog' }],
|
|
70
|
+
nextCardId: 1,
|
|
71
|
+
defaultStatus: 'backlog',
|
|
72
|
+
defaultPriority: 'medium',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
...config,
|
|
76
|
+
}, null, 2) + '\n',
|
|
77
|
+
'utf-8',
|
|
78
|
+
)
|
|
79
|
+
return {
|
|
80
|
+
workspaceDir,
|
|
81
|
+
configPath,
|
|
82
|
+
cleanup: () => fs.rmSync(workspaceDir, { recursive: true, force: true }),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function installTempCliPlugin(packageName: string, entrySource: string): () => void {
|
|
87
|
+
const packageDir = path.join(WORKSPACE_ROOT, 'node_modules', packageName)
|
|
88
|
+
fs.mkdirSync(packageDir, { recursive: true })
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(packageDir, 'package.json'),
|
|
91
|
+
JSON.stringify({ name: packageName, main: 'index.js' }, null, 2),
|
|
92
|
+
'utf-8',
|
|
93
|
+
)
|
|
94
|
+
fs.writeFileSync(path.join(packageDir, 'index.js'), entrySource, 'utf-8')
|
|
95
|
+
return () => fs.rmSync(packageDir, { recursive: true, force: true })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createCliSdkProbePackageSource(packageName: string, command: string): string {
|
|
99
|
+
return [
|
|
100
|
+
"const fs = require('node:fs')",
|
|
101
|
+
`const packageName = ${JSON.stringify(packageName)}`,
|
|
102
|
+
`const command = ${JSON.stringify(command)}`,
|
|
103
|
+
'module.exports.authIdentityPlugin = {',
|
|
104
|
+
' manifest: { id: packageName, provides: [\'auth.identity\'] },',
|
|
105
|
+
' async resolveIdentity() { return null },',
|
|
106
|
+
'}',
|
|
107
|
+
'module.exports.cliPlugin = {',
|
|
108
|
+
' manifest: { id: packageName },',
|
|
109
|
+
' command,',
|
|
110
|
+
' async run(subArgs, flags, context) {',
|
|
111
|
+
' const runWithCliAuthResult = typeof context.runWithCliAuth === \"function\"',
|
|
112
|
+
' ? await context.runWithCliAuth(() => Promise.resolve(\"ok\"))',
|
|
113
|
+
' : null',
|
|
114
|
+
' const payload = {',
|
|
115
|
+
' subArgs,',
|
|
116
|
+
' hasSdk: !!context.sdk,',
|
|
117
|
+
' hasGetConfigSnapshot: typeof context.sdk?.getConfigSnapshot === \"function\",',
|
|
118
|
+
' hasGetBoard: typeof context.sdk?.getBoard === "function",',
|
|
119
|
+
' hasGetExtension: typeof context.sdk?.getExtension === \"function\",',
|
|
120
|
+
' hasWorkspaceRootGetter: typeof context.sdk?.workspaceRoot === \"string\",',
|
|
121
|
+
' snapshotDefaultBoard: context.sdk?.getConfigSnapshot?.().defaultBoard ?? null,',
|
|
122
|
+
' defaultBoardName: context.sdk?.getBoard?.("default")?.name ?? null,',
|
|
123
|
+
' runWithCliAuthResult,',
|
|
124
|
+
' workspaceRoot: context.workspaceRoot,',
|
|
125
|
+
' flagCount: Object.keys(flags || {}).length,',
|
|
126
|
+
' }',
|
|
127
|
+
' if (process.env.KANBAN_SDK_PROBE_OUTPUT) {',
|
|
128
|
+
' fs.writeFileSync(process.env.KANBAN_SDK_PROBE_OUTPUT, JSON.stringify(payload, null, 2))',
|
|
129
|
+
' }',
|
|
130
|
+
' },',
|
|
131
|
+
'}',
|
|
132
|
+
].join('\n')
|
|
133
|
+
}
|
|
134
|
+
|
|
56
135
|
async function createCliCardStateWorkspace(): Promise<{ workspaceDir: string; configPath: string; cardId: string; cleanup: () => void }> {
|
|
57
136
|
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kanban-cli-card-state-'))
|
|
58
137
|
fs.mkdirSync(path.join(workspaceDir, '.kanban'), { recursive: true })
|
|
@@ -733,3 +812,81 @@ describe('Webhook CLI routing — plugin-owned dispatch', () => {
|
|
|
733
812
|
})
|
|
734
813
|
})
|
|
735
814
|
|
|
815
|
+
describe('CLI plugin context SDK injection regressions', () => {
|
|
816
|
+
it('passes the full public SDK to standard CLI plugin commands', async () => {
|
|
817
|
+
const packageName = `kanban-cli-sdk-probe-${Date.now()}-standard`
|
|
818
|
+
const cleanupPlugin = installTempCliPlugin(
|
|
819
|
+
packageName,
|
|
820
|
+
createCliSdkProbePackageSource(packageName, 'sdk-probe'),
|
|
821
|
+
)
|
|
822
|
+
const { workspaceDir, configPath, cleanup } = createCliWorkspace({
|
|
823
|
+
auth: {
|
|
824
|
+
'auth.identity': { provider: packageName },
|
|
825
|
+
},
|
|
826
|
+
})
|
|
827
|
+
const markerPath = path.join(workspaceDir, 'sdk-probe-standard.json')
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const result = await runCliCommand(['sdk-probe', '--config', configPath], {
|
|
831
|
+
KANBAN_SDK_PROBE_OUTPUT: markerPath,
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
expect(result.exitCode).toBe(0)
|
|
835
|
+
const payload = JSON.parse(fs.readFileSync(markerPath, 'utf-8')) as Record<string, unknown>
|
|
836
|
+
expect(payload).toMatchObject({
|
|
837
|
+
subArgs: [],
|
|
838
|
+
hasSdk: true,
|
|
839
|
+
hasGetConfigSnapshot: true,
|
|
840
|
+
hasGetBoard: true,
|
|
841
|
+
hasGetExtension: true,
|
|
842
|
+
hasWorkspaceRootGetter: true,
|
|
843
|
+
snapshotDefaultBoard: 'default',
|
|
844
|
+
defaultBoardName: 'Default',
|
|
845
|
+
runWithCliAuthResult: 'ok',
|
|
846
|
+
workspaceRoot: workspaceDir,
|
|
847
|
+
})
|
|
848
|
+
} finally {
|
|
849
|
+
cleanup()
|
|
850
|
+
cleanupPlugin()
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it('passes the full public SDK through the auth special-case CLI path', async () => {
|
|
855
|
+
const packageName = `kanban-cli-sdk-probe-${Date.now()}-auth`
|
|
856
|
+
const cleanupPlugin = installTempCliPlugin(
|
|
857
|
+
packageName,
|
|
858
|
+
createCliSdkProbePackageSource(packageName, 'auth'),
|
|
859
|
+
)
|
|
860
|
+
const { workspaceDir, configPath, cleanup } = createCliWorkspace({
|
|
861
|
+
auth: {
|
|
862
|
+
'auth.identity': { provider: packageName },
|
|
863
|
+
},
|
|
864
|
+
})
|
|
865
|
+
const markerPath = path.join(workspaceDir, 'sdk-probe-auth.json')
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
const result = await runCliCommand(['auth', 'inspect', '--config', configPath], {
|
|
869
|
+
KANBAN_SDK_PROBE_OUTPUT: markerPath,
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
expect(result.exitCode).toBe(0)
|
|
873
|
+
const payload = JSON.parse(fs.readFileSync(markerPath, 'utf-8')) as Record<string, unknown>
|
|
874
|
+
expect(payload).toMatchObject({
|
|
875
|
+
subArgs: ['inspect'],
|
|
876
|
+
hasSdk: true,
|
|
877
|
+
hasGetConfigSnapshot: true,
|
|
878
|
+
hasGetBoard: true,
|
|
879
|
+
hasGetExtension: true,
|
|
880
|
+
hasWorkspaceRootGetter: true,
|
|
881
|
+
snapshotDefaultBoard: 'default',
|
|
882
|
+
defaultBoardName: 'Default',
|
|
883
|
+
runWithCliAuthResult: 'ok',
|
|
884
|
+
workspaceRoot: workspaceDir,
|
|
885
|
+
})
|
|
886
|
+
} finally {
|
|
887
|
+
cleanup()
|
|
888
|
+
cleanupPlugin()
|
|
889
|
+
}
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
package/src/cli/index.ts
CHANGED
|
@@ -1869,7 +1869,7 @@ async function cmdAuth(sdk: KanbanSDK, positional: string[], flags: Flags, cliPl
|
|
|
1869
1869
|
if (sub !== 'status') {
|
|
1870
1870
|
const authPlugin = findCliPlugin(cliPlugins, 'auth')
|
|
1871
1871
|
if (authPlugin) {
|
|
1872
|
-
await authPlugin
|
|
1872
|
+
await runCliPlugin(authPlugin, positional, flags, workspaceRoot, sdk)
|
|
1873
1873
|
return
|
|
1874
1874
|
}
|
|
1875
1875
|
console.error(red(`Unknown auth sub-command: ${sub}`))
|
|
@@ -581,6 +581,82 @@ describe('plugin-owned MCP webhook registration', () => {
|
|
|
581
581
|
}
|
|
582
582
|
})
|
|
583
583
|
|
|
584
|
+
it('keeps widened MCP plugin contexts compatible with full public SDK reads', async () => {
|
|
585
|
+
const tools: Array<{
|
|
586
|
+
name: string
|
|
587
|
+
description: string
|
|
588
|
+
schema: Record<string, unknown>
|
|
589
|
+
handler: (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>
|
|
590
|
+
}> = []
|
|
591
|
+
|
|
592
|
+
const server = {
|
|
593
|
+
tool(
|
|
594
|
+
name: string,
|
|
595
|
+
description: string,
|
|
596
|
+
schema: Record<string, unknown>,
|
|
597
|
+
handler: (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>,
|
|
598
|
+
) {
|
|
599
|
+
tools.push({ name, description, schema, handler })
|
|
600
|
+
},
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const probePlugin = {
|
|
604
|
+
manifest: { id: 'sdk-seam-probe', provides: ['mcp.tools'] as const },
|
|
605
|
+
registerTools: (ctx: ReturnType<typeof createMcpPluginContext>) => [{
|
|
606
|
+
name: 'sdk_snapshot_probe',
|
|
607
|
+
description: 'Regression probe for widened MCP SDK context.',
|
|
608
|
+
inputSchema: () => ({}),
|
|
609
|
+
handler: async () => ({
|
|
610
|
+
content: [{
|
|
611
|
+
type: 'text' as const,
|
|
612
|
+
text: JSON.stringify({
|
|
613
|
+
hasGetConfigSnapshot: typeof ctx.sdk.getConfigSnapshot === 'function',
|
|
614
|
+
hasGetBoard: typeof ctx.sdk.getBoard === 'function',
|
|
615
|
+
hasGetExtension: typeof ctx.sdk.getExtension === 'function',
|
|
616
|
+
defaultBoard: ctx.sdk.getConfigSnapshot().defaultBoard,
|
|
617
|
+
defaultBoardName: ctx.sdk.getBoard('default').name,
|
|
618
|
+
workspaceRoot: ctx.sdk.workspaceRoot,
|
|
619
|
+
}),
|
|
620
|
+
}],
|
|
621
|
+
}),
|
|
622
|
+
}],
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const resolveSpy = vi.spyOn(pluginRegistry, 'resolveMcpPlugins').mockReturnValue([
|
|
626
|
+
mcpPlugin as never,
|
|
627
|
+
probePlugin as never,
|
|
628
|
+
])
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const registered = registerPluginMcpTools(
|
|
632
|
+
server,
|
|
633
|
+
{},
|
|
634
|
+
createMcpPluginContext({
|
|
635
|
+
sdk,
|
|
636
|
+
workspaceRoot: workspaceDir,
|
|
637
|
+
kanbanDir,
|
|
638
|
+
runWithAuth: (fn) => sdk.runWithAuth({ transport: 'mcp' }, fn),
|
|
639
|
+
}),
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
expect(registered).toContain('sdk_snapshot_probe')
|
|
643
|
+
const probeTool = tools.find((tool) => tool.name === 'sdk_snapshot_probe')
|
|
644
|
+
expect(probeTool).toBeDefined()
|
|
645
|
+
|
|
646
|
+
const result = await probeTool!.handler({})
|
|
647
|
+
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
648
|
+
hasGetConfigSnapshot: true,
|
|
649
|
+
hasGetBoard: true,
|
|
650
|
+
hasGetExtension: true,
|
|
651
|
+
defaultBoard: 'default',
|
|
652
|
+
defaultBoardName: 'Default',
|
|
653
|
+
workspaceRoot: workspaceDir,
|
|
654
|
+
})
|
|
655
|
+
} finally {
|
|
656
|
+
resolveSpy.mockRestore()
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
|
|
584
660
|
it('rejects duplicate plugin-owned MCP tool registration for webhook tools', () => {
|
|
585
661
|
const duplicatePlugin = {
|
|
586
662
|
manifest: { id: 'duplicate-webhooks-test', provides: ['mcp.tools'] as const },
|
package/src/mcp-server/index.ts
CHANGED
|
@@ -789,7 +789,7 @@ async function main(): Promise<void> {
|
|
|
789
789
|
|
|
790
790
|
server.tool(
|
|
791
791
|
'trigger_action',
|
|
792
|
-
'Trigger a named action on a card. The action name must match one of the card\'s configured actions.
|
|
792
|
+
'Trigger a named action on a card. The action name must match one of the card\'s configured actions. Emits a card.action.triggered event delivered to registered webhooks.',
|
|
793
793
|
{
|
|
794
794
|
card_id: z.string().describe('Card ID (partial match supported)'),
|
|
795
795
|
action: z.string().describe('Action name to trigger'),
|
package/src/sdk/KanbanSDK.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Comment, Card, KanbanColumn, BoardInfo, LabelDefinition, CardSortOption, LogEntry } from '../shared/types';
|
|
2
2
|
import type { CardDisplaySettings, Priority } from '../shared/types';
|
|
3
|
-
import type { BoardConfig, ResolvedCapabilities, Webhook } from '../shared/config';
|
|
3
|
+
import type { BoardConfig, KanbanConfig, ResolvedCapabilities, Webhook } from '../shared/config';
|
|
4
4
|
import type { CreateCardInput, SDKEvent, SDKEventType, SDKOptions, SubmitFormInput, SubmitFormResult, AuthContext, AuthDecision, SDKBeforeEventType, SDKAfterEventType, CardStateStatus, CardUnreadSummary } from './types';
|
|
5
5
|
import type { EventBusAnyListener, EventBusWaitOptions } from './eventBus';
|
|
6
6
|
import { EventBus } from './eventBus';
|
|
@@ -67,6 +67,9 @@ export interface WebhookStatus {
|
|
|
67
67
|
}
|
|
68
68
|
/** Active card-state provider metadata for diagnostics and host surfaces. */
|
|
69
69
|
export type CardStateRuntimeStatus = CardStateStatus;
|
|
70
|
+
type ReadonlySnapshot<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer U)[] ? readonly ReadonlySnapshot<U>[] : T extends object ? {
|
|
71
|
+
readonly [K in keyof T]: ReadonlySnapshot<T[K]>;
|
|
72
|
+
} : T;
|
|
70
73
|
/**
|
|
71
74
|
* Optional search and sort inputs for {@link KanbanSDK.listCards}.
|
|
72
75
|
*
|
|
@@ -524,6 +527,23 @@ export declare class KanbanSDK {
|
|
|
524
527
|
* ```
|
|
525
528
|
*/
|
|
526
529
|
get workspaceRoot(): string;
|
|
530
|
+
/**
|
|
531
|
+
* Returns a cloned read-only snapshot of the current workspace config.
|
|
532
|
+
*
|
|
533
|
+
* The returned snapshot is created from a fresh config read and deep-cloned
|
|
534
|
+
* before being returned, so callers receive an isolated view of the current
|
|
535
|
+
* `.kanban.json` state rather than a live mutable runtime object. Mutating the
|
|
536
|
+
* returned snapshot does not update persisted config or affect this SDK instance.
|
|
537
|
+
*
|
|
538
|
+
* @returns A cloned read-only snapshot of the current {@link KanbanConfig}.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```ts
|
|
542
|
+
* const config = sdk.getConfigSnapshot()
|
|
543
|
+
* console.log(config.defaultBoard)
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
getConfigSnapshot(): ReadonlySnapshot<KanbanConfig>;
|
|
527
547
|
/** @internal */
|
|
528
548
|
_resolveBoardId(boardId?: string): string;
|
|
529
549
|
/** @internal */
|
|
@@ -958,21 +978,17 @@ export declare class KanbanSDK {
|
|
|
958
978
|
*/
|
|
959
979
|
submitForm(input: SubmitFormInput): Promise<SubmitFormResult>;
|
|
960
980
|
/**
|
|
961
|
-
* Triggers a named action for a card
|
|
962
|
-
* configured in `.kanban.json`.
|
|
981
|
+
* Triggers a named action for a card.
|
|
963
982
|
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
* ```
|
|
983
|
+
* Validates the card, appends an activity log entry, and emits the
|
|
984
|
+
* `card.action.triggered` after-event so registered webhooks receive
|
|
985
|
+
* the action payload automatically.
|
|
968
986
|
*
|
|
969
987
|
* @param cardId - The ID of the card to trigger the action for.
|
|
970
988
|
* @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
|
|
971
989
|
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
972
|
-
* @returns A promise
|
|
973
|
-
* @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
|
|
990
|
+
* @returns A promise that resolves when the action has been processed.
|
|
974
991
|
* @throws {Error} If the card is not found.
|
|
975
|
-
* @throws {Error} If the webhook responds with a non-2xx status.
|
|
976
992
|
*
|
|
977
993
|
* @example
|
|
978
994
|
* ```ts
|
package/src/sdk/KanbanSDK.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Comment, Card, KanbanColumn, BoardInfo, LabelDefinition, CardSortO
|
|
|
4
4
|
import type { CardDisplaySettings, Priority } from '../shared/types'
|
|
5
5
|
import { DELETED_STATUS_ID } from '../shared/types'
|
|
6
6
|
import { readConfig, normalizeStorageCapabilities, normalizeAuthCapabilities, normalizeWebhookCapabilities, normalizeCardStateCapabilities } from '../shared/config'
|
|
7
|
-
import type { BoardConfig, ProviderRef, ResolvedCapabilities, ResolvedWebhookCapabilities, ResolvedCardStateCapabilities, Webhook } from '../shared/config'
|
|
7
|
+
import type { BoardConfig, KanbanConfig, ProviderRef, ResolvedCapabilities, ResolvedWebhookCapabilities, ResolvedCardStateCapabilities, Webhook } from '../shared/config'
|
|
8
8
|
import type { ResolvedAuthCapabilities } from '../shared/config'
|
|
9
9
|
import type { CreateCardInput, SDKEvent, SDKEventHandler, SDKEventType, SDKOptions, SubmitFormInput, SubmitFormResult, AuthContext, AuthDecision, SDKEventListenerPlugin, BeforeEventPayload, AfterEventPayload, SDKBeforeEventType, SDKAfterEventType, CardStateStatus, CardOpenStateValue, CardUnreadSummary } from './types'
|
|
10
10
|
import type { EventBusAnyListener, EventBusWaitOptions } from './eventBus'
|
|
@@ -113,6 +113,15 @@ type MethodInput<TMethod> =
|
|
|
113
113
|
? TFirst
|
|
114
114
|
: Record<string, unknown>
|
|
115
115
|
|
|
116
|
+
type ReadonlySnapshot<T> =
|
|
117
|
+
T extends (...args: never[]) => unknown
|
|
118
|
+
? T
|
|
119
|
+
: T extends readonly (infer U)[]
|
|
120
|
+
? readonly ReadonlySnapshot<U>[]
|
|
121
|
+
: T extends object
|
|
122
|
+
? { readonly [K in keyof T]: ReadonlySnapshot<T[K]> }
|
|
123
|
+
: T
|
|
124
|
+
|
|
116
125
|
/**
|
|
117
126
|
* Active webhook provider metadata for diagnostics and host surfaces.
|
|
118
127
|
*
|
|
@@ -1158,6 +1167,26 @@ export class KanbanSDK {
|
|
|
1158
1167
|
return path.dirname(this.kanbanDir)
|
|
1159
1168
|
}
|
|
1160
1169
|
|
|
1170
|
+
/**
|
|
1171
|
+
* Returns a cloned read-only snapshot of the current workspace config.
|
|
1172
|
+
*
|
|
1173
|
+
* The returned snapshot is created from a fresh config read and deep-cloned
|
|
1174
|
+
* before being returned, so callers receive an isolated view of the current
|
|
1175
|
+
* `.kanban.json` state rather than a live mutable runtime object. Mutating the
|
|
1176
|
+
* returned snapshot does not update persisted config or affect this SDK instance.
|
|
1177
|
+
*
|
|
1178
|
+
* @returns A cloned read-only snapshot of the current {@link KanbanConfig}.
|
|
1179
|
+
*
|
|
1180
|
+
* @example
|
|
1181
|
+
* ```ts
|
|
1182
|
+
* const config = sdk.getConfigSnapshot()
|
|
1183
|
+
* console.log(config.defaultBoard)
|
|
1184
|
+
* ```
|
|
1185
|
+
*/
|
|
1186
|
+
getConfigSnapshot(): ReadonlySnapshot<KanbanConfig> {
|
|
1187
|
+
return structuredClone(readConfig(this.workspaceRoot)) as ReadonlySnapshot<KanbanConfig>
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1161
1190
|
// --- Board resolution helpers ---
|
|
1162
1191
|
|
|
1163
1192
|
/** @internal */
|
|
@@ -1737,21 +1766,17 @@ export class KanbanSDK {
|
|
|
1737
1766
|
}
|
|
1738
1767
|
|
|
1739
1768
|
/**
|
|
1740
|
-
* Triggers a named action for a card
|
|
1741
|
-
* configured in `.kanban.json`.
|
|
1769
|
+
* Triggers a named action for a card.
|
|
1742
1770
|
*
|
|
1743
|
-
*
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
1746
|
-
* ```
|
|
1771
|
+
* Validates the card, appends an activity log entry, and emits the
|
|
1772
|
+
* `card.action.triggered` after-event so registered webhooks receive
|
|
1773
|
+
* the action payload automatically.
|
|
1747
1774
|
*
|
|
1748
1775
|
* @param cardId - The ID of the card to trigger the action for.
|
|
1749
1776
|
* @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
|
|
1750
1777
|
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
1751
|
-
* @returns A promise
|
|
1752
|
-
* @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
|
|
1778
|
+
* @returns A promise that resolves when the action has been processed.
|
|
1753
1779
|
* @throws {Error} If the card is not found.
|
|
1754
|
-
* @throws {Error} If the webhook responds with a non-2xx status.
|
|
1755
1780
|
*
|
|
1756
1781
|
* @example
|
|
1757
1782
|
* ```ts
|
|
@@ -1761,7 +1786,8 @@ export class KanbanSDK {
|
|
|
1761
1786
|
*/
|
|
1762
1787
|
async triggerAction(cardId: string, action: string, boardId?: string): Promise<void> {
|
|
1763
1788
|
const mergedInput = await this._runBeforeEvent<MethodInput<typeof Cards.triggerAction>>('card.action.trigger', { cardId, action, boardId }, undefined, boardId)
|
|
1764
|
-
|
|
1789
|
+
const payload = await Cards.triggerAction(this, mergedInput)
|
|
1790
|
+
this._runAfterEvent('card.action.triggered', payload, undefined, payload.board)
|
|
1765
1791
|
}
|
|
1766
1792
|
|
|
1767
1793
|
/**
|
|
@@ -155,6 +155,62 @@ describe('KanbanSDK', () => {
|
|
|
155
155
|
})
|
|
156
156
|
})
|
|
157
157
|
|
|
158
|
+
describe('getConfigSnapshot', () => {
|
|
159
|
+
it('returns an isolated clone that cannot mutate subsequent SDK reads or persisted config', () => {
|
|
160
|
+
writeWorkspaceConfig(workspaceDir, {
|
|
161
|
+
defaultBoard: 'default',
|
|
162
|
+
kanbanDirectory: '.kanban',
|
|
163
|
+
boards: {
|
|
164
|
+
default: {
|
|
165
|
+
name: 'Default',
|
|
166
|
+
columns: [{ id: 'backlog', name: 'Backlog' }],
|
|
167
|
+
nextCardId: 1,
|
|
168
|
+
defaultStatus: 'backlog',
|
|
169
|
+
defaultPriority: 'medium',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
plugins: {
|
|
173
|
+
'auth.identity': {
|
|
174
|
+
provider: 'kl-auth-plugin',
|
|
175
|
+
options: {
|
|
176
|
+
users: [{ username: 'alice', password: '$2b$12$existing-hash' }],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
webhooks: [
|
|
181
|
+
{ id: 'wh_snapshot', url: 'https://example.com/original', events: ['*'], active: true },
|
|
182
|
+
],
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const firstSnapshot = sdk.getConfigSnapshot() as any
|
|
186
|
+
firstSnapshot.defaultBoard = 'mutated-board'
|
|
187
|
+
firstSnapshot.boards.default.name = 'Mutated Board'
|
|
188
|
+
firstSnapshot.webhooks[0].url = 'https://example.com/mutated'
|
|
189
|
+
firstSnapshot.plugins['auth.identity'].options.users.push({ username: 'mallory', password: 'bad-hash' })
|
|
190
|
+
|
|
191
|
+
const secondSnapshot = sdk.getConfigSnapshot() as any
|
|
192
|
+
const persisted = JSON.parse(fs.readFileSync(path.join(workspaceDir, '.kanban.json'), 'utf-8')) as any
|
|
193
|
+
|
|
194
|
+
expect(secondSnapshot.defaultBoard).toBe('default')
|
|
195
|
+
expect(secondSnapshot.boards.default.name).toBe('Default')
|
|
196
|
+
expect(secondSnapshot.webhooks[0].url).toBe('https://example.com/original')
|
|
197
|
+
expect(secondSnapshot.plugins['auth.identity'].options.users).toEqual([
|
|
198
|
+
{ username: 'alice', password: '$2b$12$existing-hash' },
|
|
199
|
+
])
|
|
200
|
+
expect(sdk.listWebhooks()).toEqual([
|
|
201
|
+
{ id: 'wh_snapshot', url: 'https://example.com/original', events: ['*'], active: true },
|
|
202
|
+
])
|
|
203
|
+
expect(persisted.defaultBoard).toBe('default')
|
|
204
|
+
expect(persisted.boards.default.name).toBe('Default')
|
|
205
|
+
expect(persisted.webhooks).toEqual([
|
|
206
|
+
{ id: 'wh_snapshot', url: 'https://example.com/original', events: ['*'], active: true },
|
|
207
|
+
])
|
|
208
|
+
expect(persisted.plugins['auth.identity'].options.users).toEqual([
|
|
209
|
+
{ username: 'alice', password: '$2b$12$existing-hash' },
|
|
210
|
+
])
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
158
214
|
describe('getCardStateStatus', () => {
|
|
159
215
|
it('reports builtin backend status and allows the default actor when auth.identity is noop', () => {
|
|
160
216
|
const status = sdk.getCardStateStatus()
|
|
@@ -1080,10 +1136,18 @@ describe('KanbanSDK', () => {
|
|
|
1080
1136
|
expect(reloaded?.actions).toBeUndefined()
|
|
1081
1137
|
})
|
|
1082
1138
|
|
|
1083
|
-
it('should
|
|
1139
|
+
it('should append an activity log entry even when no actionWebhookUrl is configured', async () => {
|
|
1084
1140
|
await sdk.init()
|
|
1085
1141
|
const card = await sdk.createCard({ content: '# Card', actions: ['retry'] })
|
|
1086
|
-
await expect(sdk.triggerAction(card.id, 'retry')).
|
|
1142
|
+
await expect(sdk.triggerAction(card.id, 'retry')).resolves.toBeUndefined()
|
|
1143
|
+
|
|
1144
|
+
const logs = await sdk.listLogs(card.id)
|
|
1145
|
+
expect(logs).toHaveLength(1)
|
|
1146
|
+
expect(logs[0]).toMatchObject({
|
|
1147
|
+
source: 'system',
|
|
1148
|
+
text: 'Action triggered: `retry`',
|
|
1149
|
+
object: { action: 'retry' },
|
|
1150
|
+
})
|
|
1087
1151
|
})
|
|
1088
1152
|
|
|
1089
1153
|
it('should throw if card not found', async () => {
|
|
@@ -1094,45 +1158,36 @@ describe('KanbanSDK', () => {
|
|
|
1094
1158
|
await expect(sdk.triggerAction('nonexistent', 'retry')).rejects.toThrow('Card not found')
|
|
1095
1159
|
})
|
|
1096
1160
|
|
|
1097
|
-
it('should
|
|
1161
|
+
it('should not call fetch directly; webhook delivery is delegated to plugin after-events', async () => {
|
|
1098
1162
|
await sdk.init()
|
|
1099
1163
|
const card = await sdk.createCard({ content: '# My Card', actions: ['retry'] })
|
|
1100
1164
|
|
|
1101
|
-
const { readConfig, writeConfig } = await import('../../shared/config')
|
|
1102
|
-
const config = readConfig(sdk.workspaceRoot)
|
|
1103
|
-
writeConfig(sdk.workspaceRoot, { ...config, actionWebhookUrl: 'https://example.com/webhook' })
|
|
1104
|
-
|
|
1105
1165
|
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: 'OK' })
|
|
1106
1166
|
vi.stubGlobal('fetch', mockFetch)
|
|
1107
1167
|
|
|
1108
1168
|
await sdk.triggerAction(card.id, 'retry')
|
|
1109
1169
|
|
|
1110
|
-
expect(mockFetch).
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
expect(
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
expect(body.board).toBe('default')
|
|
1118
|
-
expect(body.list).toBe(card.status)
|
|
1119
|
-
expect(body.card.id).toBe(card.id)
|
|
1120
|
-
expect(body.card.filePath).toBeUndefined()
|
|
1170
|
+
expect(mockFetch).not.toHaveBeenCalled()
|
|
1171
|
+
|
|
1172
|
+
const logs = await sdk.listLogs(card.id)
|
|
1173
|
+
expect(logs.at(-1)).toMatchObject({
|
|
1174
|
+
text: 'Action triggered: `retry`',
|
|
1175
|
+
object: { action: 'retry' },
|
|
1176
|
+
})
|
|
1121
1177
|
|
|
1122
1178
|
vi.unstubAllGlobals()
|
|
1123
1179
|
})
|
|
1124
1180
|
|
|
1125
|
-
it('should
|
|
1181
|
+
it('should stay side-effect free with respect to direct webhook transport failures', async () => {
|
|
1126
1182
|
await sdk.init()
|
|
1127
1183
|
const card = await sdk.createCard({ content: '# My Card', actions: ['retry'] })
|
|
1128
1184
|
|
|
1129
|
-
const { readConfig, writeConfig } = await import('../../shared/config')
|
|
1130
|
-
const config = readConfig(sdk.workspaceRoot)
|
|
1131
|
-
writeConfig(sdk.workspaceRoot, { ...config, actionWebhookUrl: 'https://example.com/webhook' })
|
|
1132
|
-
|
|
1133
1185
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error' }))
|
|
1134
1186
|
|
|
1135
|
-
await expect(sdk.triggerAction(card.id, 'retry')).
|
|
1187
|
+
await expect(sdk.triggerAction(card.id, 'retry')).resolves.toBeUndefined()
|
|
1188
|
+
|
|
1189
|
+
const logs = await sdk.listLogs(card.id)
|
|
1190
|
+
expect(logs.at(-1)?.text).toBe('Action triggered: `retry`')
|
|
1136
1191
|
|
|
1137
1192
|
vi.unstubAllGlobals()
|
|
1138
1193
|
})
|
|
@@ -171,6 +171,7 @@ const AFTER_ENTRIES = [
|
|
|
171
171
|
{ event: 'board.updated' as SDKAfterEventType, resource: 'board' as KanbanResource, label: 'Board updated' },
|
|
172
172
|
{ event: 'board.deleted' as SDKAfterEventType, resource: 'board' as KanbanResource, label: 'Board deleted' },
|
|
173
173
|
{ event: 'board.action' as SDKAfterEventType, resource: 'board' as KanbanResource, label: 'Board action triggered' },
|
|
174
|
+
{ event: 'card.action.triggered' as SDKAfterEventType, resource: 'card' as KanbanResource, label: 'Card action triggered' },
|
|
174
175
|
{ event: 'board.log.added' as SDKAfterEventType, resource: 'board' as KanbanResource, label: 'Board log added' },
|
|
175
176
|
{ event: 'board.log.cleared' as SDKAfterEventType, resource: 'board' as KanbanResource, label: 'Board log cleared' },
|
|
176
177
|
// card logs
|
package/src/sdk/modules/cards.ts
CHANGED
|
@@ -452,39 +452,21 @@ export async function updateCard(
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
/**
|
|
455
|
-
* Triggers a named action for a card
|
|
455
|
+
* Triggers a named action for a card.
|
|
456
|
+
*
|
|
457
|
+
* Validates the card exists, appends an activity log entry, and returns the
|
|
458
|
+
* action payload. Webhook delivery is handled by the webhook plugin via the
|
|
459
|
+
* `card.action.triggered` after-event emitted by {@link KanbanSDK.triggerAction}.
|
|
456
460
|
*/
|
|
457
461
|
export async function triggerAction(
|
|
458
462
|
ctx: SDKContext,
|
|
459
463
|
{ cardId, action, boardId }: { cardId: string; action: string; boardId?: string }
|
|
460
|
-
): Promise<
|
|
461
|
-
const config = readConfig(ctx.workspaceRoot)
|
|
462
|
-
const { actionWebhookUrl } = config
|
|
463
|
-
if (!actionWebhookUrl) {
|
|
464
|
-
throw new Error('No action webhook URL configured. Set actionWebhookUrl in .kanban.json')
|
|
465
|
-
}
|
|
466
|
-
|
|
464
|
+
): Promise<{ action: string; board: string; list: string; card: Omit<Card, 'filePath'> }> {
|
|
467
465
|
const card = await getCard(ctx, { cardId, boardId })
|
|
468
466
|
if (!card) throw new Error(`Card not found: ${cardId}`)
|
|
469
467
|
|
|
470
468
|
const resolvedBoardId = card.boardId || ctx._resolveBoardId(boardId)
|
|
471
469
|
|
|
472
|
-
const payload = {
|
|
473
|
-
action,
|
|
474
|
-
board: resolvedBoardId,
|
|
475
|
-
list: card.status,
|
|
476
|
-
card: sanitizeCard(card),
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const response = await fetch(actionWebhookUrl, {
|
|
480
|
-
method: 'POST',
|
|
481
|
-
headers: { 'Content-Type': 'application/json' },
|
|
482
|
-
body: JSON.stringify(payload),
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
if (!response.ok) {
|
|
486
|
-
throw new Error(`Action webhook responded with ${response.status}: ${response.statusText}`)
|
|
487
|
-
}
|
|
488
470
|
await appendActivityLog(ctx, {
|
|
489
471
|
cardId,
|
|
490
472
|
boardId: resolvedBoardId,
|
|
@@ -494,6 +476,13 @@ export async function triggerAction(
|
|
|
494
476
|
action,
|
|
495
477
|
},
|
|
496
478
|
}).catch(() => {})
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
action,
|
|
482
|
+
board: resolvedBoardId,
|
|
483
|
+
list: card.status,
|
|
484
|
+
card: sanitizeCard(card),
|
|
485
|
+
}
|
|
497
486
|
}
|
|
498
487
|
|
|
499
488
|
/**
|
|
@@ -337,6 +337,13 @@ export type StandaloneHttpHandler = (request: StandaloneHttpRequestContext) => P
|
|
|
337
337
|
* resolved the active workspace capability selections.
|
|
338
338
|
*/
|
|
339
339
|
export interface StandaloneHttpPluginRegistrationOptions {
|
|
340
|
+
/**
|
|
341
|
+
* Active SDK instance backing the standalone runtime, when provided by the host.
|
|
342
|
+
*
|
|
343
|
+
* Plugin registration code may use the full public {@link KanbanSDK} surface,
|
|
344
|
+
* including `getConfigSnapshot()`, when this seam is available.
|
|
345
|
+
*/
|
|
346
|
+
readonly sdk?: KanbanSDK;
|
|
340
347
|
/** Absolute workspace root containing `.kanban.json`. */
|
|
341
348
|
readonly workspaceRoot: string;
|
|
342
349
|
/** Absolute workspace `.kanban` directory. */
|