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.
Files changed (39) hide show
  1. package/dist/cli.js +257 -90
  2. package/dist/extension.js +245 -78
  3. package/dist/mcp-server.js +117 -33
  4. package/dist/sdk/index.cjs +117 -32
  5. package/dist/sdk/index.mjs +117 -32
  6. package/dist/sdk/sdk/KanbanSDK.d.ts +27 -10
  7. package/dist/sdk/sdk/modules/cards.d.ts +11 -2
  8. package/dist/sdk/sdk/plugins/index.d.ts +7 -0
  9. package/dist/sdk/sdk/types.d.ts +12 -27
  10. package/dist/sdk/shared/config.d.ts +17 -1
  11. package/dist/standalone-webview/index.js +38 -38
  12. package/dist/standalone-webview/index.js.map +1 -1
  13. package/dist/standalone.js +307 -125
  14. package/package.json +1 -1
  15. package/src/cli/index.test.ts +157 -0
  16. package/src/cli/index.ts +1 -1
  17. package/src/mcp-server/index.test.ts +76 -0
  18. package/src/mcp-server/index.ts +1 -1
  19. package/src/sdk/KanbanSDK.d.ts +26 -10
  20. package/src/sdk/KanbanSDK.ts +37 -11
  21. package/src/sdk/__tests__/KanbanSDK.test.ts +79 -24
  22. package/src/sdk/integrationCatalog.ts +1 -0
  23. package/src/sdk/modules/cards.ts +13 -24
  24. package/src/sdk/plugins/index.d.ts +7 -0
  25. package/src/sdk/plugins/index.ts +17 -2
  26. package/src/sdk/types.d.ts +10 -26
  27. package/src/sdk/types.ts +11 -24
  28. package/src/sdk/webhooks.ts +19 -2
  29. package/src/shared/config.ts +130 -2
  30. package/src/standalone/__tests__/server.integration.test.ts +81 -2
  31. package/src/standalone/internal/runtime.ts +11 -6
  32. package/src/standalone/internal/websocket.ts +13 -3
  33. package/src/standalone/server.ts +67 -9
  34. package/src/standalone/watcherSetup.ts +9 -0
  35. package/src/webview/standalone-shim.ts +2 -1
  36. package/tmp/screenshots-workspace/.kanban/.active-card.json +5 -0
  37. package/tmp/screenshots-workspace/.kanban/boards/default/deleted/1-dddd.md +17 -0
  38. package/tmp/screenshots-workspace/.kanban/boards/default/deleted/attachments/1.log +1 -0
  39. package/tmp/screenshots-workspace/.kanban.json +59 -0
@@ -17664,11 +17664,87 @@ function migrateConfigV1ToV2(raw) {
17664
17664
  }
17665
17665
  return v2;
17666
17666
  }
17667
+ function loadDotEnv(dir) {
17668
+ const envPath = path.join(dir, ".env");
17669
+ let content;
17670
+ try {
17671
+ content = fs.readFileSync(envPath, "utf-8");
17672
+ } catch {
17673
+ return;
17674
+ }
17675
+ for (const line of content.split("\n")) {
17676
+ const trimmed = line.trim();
17677
+ if (!trimmed || trimmed.startsWith("#"))
17678
+ continue;
17679
+ const eqIdx = trimmed.indexOf("=");
17680
+ if (eqIdx < 1)
17681
+ continue;
17682
+ const key = trimmed.slice(0, eqIdx).trim();
17683
+ let val = trimmed.slice(eqIdx + 1).trim();
17684
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
17685
+ val = val.slice(1, -1);
17686
+ }
17687
+ if (process.env[key] === void 0) {
17688
+ process.env[key] = val;
17689
+ }
17690
+ }
17691
+ }
17692
+ function resolveConfigEnvVars(node, configFileName, nodePath = "") {
17693
+ const isFormDefaultDataPath = /^\.forms\.(?:[^.]+|"[^"]+")\.data(?:$|[.\[])/.test(nodePath);
17694
+ if (isFormDefaultDataPath) {
17695
+ return node;
17696
+ }
17697
+ if (typeof node === "string") {
17698
+ return node.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
17699
+ const envValue = process.env[varName];
17700
+ if (envValue === void 0) {
17701
+ throw new Error(
17702
+ `missing ${varName} in ${configFileName}: ${nodePath} "${node}"`
17703
+ );
17704
+ }
17705
+ return envValue;
17706
+ });
17707
+ }
17708
+ if (Array.isArray(node)) {
17709
+ for (let i = 0; i < node.length; i++) {
17710
+ node[i] = resolveConfigEnvVars(node[i], configFileName, `${nodePath}[${i}]`);
17711
+ }
17712
+ return node;
17713
+ }
17714
+ if (node !== null && typeof node === "object") {
17715
+ const obj = node;
17716
+ for (const key of Object.keys(obj)) {
17717
+ const jsonKey = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
17718
+ const childPath = nodePath ? `${nodePath}.${jsonKey}` : `.${jsonKey}`;
17719
+ obj[key] = resolveConfigEnvVars(obj[key], configFileName, childPath);
17720
+ }
17721
+ return obj;
17722
+ }
17723
+ return node;
17724
+ }
17667
17725
  function readConfig(workspaceRoot) {
17668
17726
  const filePath = configPath(workspaceRoot);
17669
17727
  const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } };
17728
+ let raw;
17729
+ try {
17730
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17731
+ } catch {
17732
+ return defaults;
17733
+ }
17734
+ loadDotEnv(workspaceRoot);
17735
+ try {
17736
+ resolveConfigEnvVars(raw, CONFIG_FILENAME);
17737
+ } catch (err) {
17738
+ const msg = err instanceof Error ? err.message : String(err);
17739
+ process.stderr.write(`
17740
+ Configuration error: ${msg}
17741
+
17742
+ Set the missing environment variable before starting the server.
17743
+
17744
+ `);
17745
+ process.exit(1);
17746
+ }
17670
17747
  try {
17671
- const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17672
17748
  const isV1 = raw.version === 1 || !raw.version && !(typeof raw.boards === "object" && raw.boards !== null && !Array.isArray(raw.boards));
17673
17749
  if (isV1) {
17674
17750
  const v2 = migrateConfigV1ToV2(raw);
@@ -21268,7 +21344,7 @@ function resolveAuthIdentityPlugin(ref) {
21268
21344
  if (ref.provider === "rbac")
21269
21345
  return RBAC_IDENTITY_PLUGIN;
21270
21346
  const packageName = AUTH_PROVIDER_ALIASES.get(ref.provider) ?? ref.provider;
21271
- return loadExternalAuthIdentityPlugin(packageName, ref.provider);
21347
+ return loadExternalAuthIdentityPlugin(packageName, ref.provider, ref.options);
21272
21348
  }
21273
21349
  function resolveAuthPolicyPlugin(ref) {
21274
21350
  if (ref.provider === "noop")
@@ -21494,8 +21570,13 @@ function selectAuthPolicyPlugin(mod, providerId) {
21494
21570
  return direct;
21495
21571
  return null;
21496
21572
  }
21497
- function loadExternalAuthIdentityPlugin(packageName, providerId) {
21573
+ function loadExternalAuthIdentityPlugin(packageName, providerId, options) {
21498
21574
  const mod = loadExternalModule(packageName);
21575
+ if (options !== void 0 && typeof mod.createAuthIdentityPlugin === "function") {
21576
+ const created = mod.createAuthIdentityPlugin(options);
21577
+ if (isValidAuthIdentityPlugin(created, providerId))
21578
+ return created;
21579
+ }
21499
21580
  const plugin = selectAuthIdentityPlugin(mod, providerId);
21500
21581
  if (!plugin) {
21501
21582
  throw new Error(
@@ -24565,29 +24646,10 @@ async function updateCard(ctx, { cardId, updates, boardId }) {
24565
24646
  return card;
24566
24647
  }
24567
24648
  async function triggerAction(ctx, { cardId, action, boardId }) {
24568
- const config = readConfig(ctx.workspaceRoot);
24569
- const { actionWebhookUrl } = config;
24570
- if (!actionWebhookUrl) {
24571
- throw new Error("No action webhook URL configured. Set actionWebhookUrl in .kanban.json");
24572
- }
24573
24649
  const card = await getCard(ctx, { cardId, boardId });
24574
24650
  if (!card)
24575
24651
  throw new Error(`Card not found: ${cardId}`);
24576
24652
  const resolvedBoardId = card.boardId || ctx._resolveBoardId(boardId);
24577
- const payload = {
24578
- action,
24579
- board: resolvedBoardId,
24580
- list: card.status,
24581
- card: sanitizeCard(card)
24582
- };
24583
- const response = await fetch(actionWebhookUrl, {
24584
- method: "POST",
24585
- headers: { "Content-Type": "application/json" },
24586
- body: JSON.stringify(payload)
24587
- });
24588
- if (!response.ok) {
24589
- throw new Error(`Action webhook responded with ${response.status}: ${response.statusText}`);
24590
- }
24591
24653
  await appendActivityLog(ctx, {
24592
24654
  cardId,
24593
24655
  boardId: resolvedBoardId,
@@ -24598,6 +24660,12 @@ async function triggerAction(ctx, { cardId, action, boardId }) {
24598
24660
  }
24599
24661
  }).catch(() => {
24600
24662
  });
24663
+ return {
24664
+ action,
24665
+ board: resolvedBoardId,
24666
+ list: card.status,
24667
+ card: sanitizeCard(card)
24668
+ };
24601
24669
  }
24602
24670
  async function submitForm(ctx, input) {
24603
24671
  const card = await getCard(ctx, { cardId: input.cardId, boardId: input.boardId });
@@ -26014,6 +26082,25 @@ var KanbanSDK = class _KanbanSDK {
26014
26082
  get workspaceRoot() {
26015
26083
  return path14.dirname(this.kanbanDir);
26016
26084
  }
26085
+ /**
26086
+ * Returns a cloned read-only snapshot of the current workspace config.
26087
+ *
26088
+ * The returned snapshot is created from a fresh config read and deep-cloned
26089
+ * before being returned, so callers receive an isolated view of the current
26090
+ * `.kanban.json` state rather than a live mutable runtime object. Mutating the
26091
+ * returned snapshot does not update persisted config or affect this SDK instance.
26092
+ *
26093
+ * @returns A cloned read-only snapshot of the current {@link KanbanConfig}.
26094
+ *
26095
+ * @example
26096
+ * ```ts
26097
+ * const config = sdk.getConfigSnapshot()
26098
+ * console.log(config.defaultBoard)
26099
+ * ```
26100
+ */
26101
+ getConfigSnapshot() {
26102
+ return structuredClone(readConfig(this.workspaceRoot));
26103
+ }
26017
26104
  // --- Board resolution helpers ---
26018
26105
  /** @internal */
26019
26106
  _resolveBoardId(boardId) {
@@ -26480,21 +26567,17 @@ var KanbanSDK = class _KanbanSDK {
26480
26567
  return result;
26481
26568
  }
26482
26569
  /**
26483
- * Triggers a named action for a card by POSTing to the global `actionWebhookUrl`
26484
- * configured in `.kanban.json`.
26570
+ * Triggers a named action for a card.
26485
26571
  *
26486
- * The payload sent to the webhook is:
26487
- * ```json
26488
- * { "action": "retry", "board": "default", "list": "in-progress", "card": { ...sanitizedCard } }
26489
- * ```
26572
+ * Validates the card, appends an activity log entry, and emits the
26573
+ * `card.action.triggered` after-event so registered webhooks receive
26574
+ * the action payload automatically.
26490
26575
  *
26491
26576
  * @param cardId - The ID of the card to trigger the action for.
26492
26577
  * @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
26493
26578
  * @param boardId - Optional board ID. Defaults to the workspace's default board.
26494
- * @returns A promise resolving when the webhook responds with 2xx.
26495
- * @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
26579
+ * @returns A promise that resolves when the action has been processed.
26496
26580
  * @throws {Error} If the card is not found.
26497
- * @throws {Error} If the webhook responds with a non-2xx status.
26498
26581
  *
26499
26582
  * @example
26500
26583
  * ```ts
@@ -26504,7 +26587,8 @@ var KanbanSDK = class _KanbanSDK {
26504
26587
  */
26505
26588
  async triggerAction(cardId, action, boardId) {
26506
26589
  const mergedInput = await this._runBeforeEvent("card.action.trigger", { cardId, action, boardId }, void 0, boardId);
26507
- return triggerAction(this, mergedInput);
26590
+ const payload = await triggerAction(this, mergedInput);
26591
+ this._runAfterEvent("card.action.triggered", payload, void 0, payload.board);
26508
26592
  }
26509
26593
  /**
26510
26594
  * Moves a card to a different status column and/or position within that column.
@@ -28159,7 +28243,7 @@ async function main() {
28159
28243
  );
28160
28244
  server.tool(
28161
28245
  "trigger_action",
28162
- "Trigger a named action on a card. The action name must match one of the card's configured actions. Calls the configured action webhook URL with the action name and card details.",
28246
+ "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.",
28163
28247
  {
28164
28248
  card_id: import_zod.z.string().describe("Card ID (partial match supported)"),
28165
28249
  action: import_zod.z.string().describe("Action name to trigger"),
@@ -17694,11 +17694,87 @@ function migrateConfigV1ToV2(raw) {
17694
17694
  }
17695
17695
  return v2;
17696
17696
  }
17697
+ function loadDotEnv(dir) {
17698
+ const envPath = path.join(dir, ".env");
17699
+ let content;
17700
+ try {
17701
+ content = fs.readFileSync(envPath, "utf-8");
17702
+ } catch {
17703
+ return;
17704
+ }
17705
+ for (const line of content.split("\n")) {
17706
+ const trimmed = line.trim();
17707
+ if (!trimmed || trimmed.startsWith("#"))
17708
+ continue;
17709
+ const eqIdx = trimmed.indexOf("=");
17710
+ if (eqIdx < 1)
17711
+ continue;
17712
+ const key = trimmed.slice(0, eqIdx).trim();
17713
+ let val = trimmed.slice(eqIdx + 1).trim();
17714
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
17715
+ val = val.slice(1, -1);
17716
+ }
17717
+ if (process.env[key] === void 0) {
17718
+ process.env[key] = val;
17719
+ }
17720
+ }
17721
+ }
17722
+ function resolveConfigEnvVars(node, configFileName, nodePath = "") {
17723
+ const isFormDefaultDataPath = /^\.forms\.(?:[^.]+|"[^"]+")\.data(?:$|[.\[])/.test(nodePath);
17724
+ if (isFormDefaultDataPath) {
17725
+ return node;
17726
+ }
17727
+ if (typeof node === "string") {
17728
+ return node.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
17729
+ const envValue = process.env[varName];
17730
+ if (envValue === void 0) {
17731
+ throw new Error(
17732
+ `missing ${varName} in ${configFileName}: ${nodePath} "${node}"`
17733
+ );
17734
+ }
17735
+ return envValue;
17736
+ });
17737
+ }
17738
+ if (Array.isArray(node)) {
17739
+ for (let i = 0; i < node.length; i++) {
17740
+ node[i] = resolveConfigEnvVars(node[i], configFileName, `${nodePath}[${i}]`);
17741
+ }
17742
+ return node;
17743
+ }
17744
+ if (node !== null && typeof node === "object") {
17745
+ const obj = node;
17746
+ for (const key of Object.keys(obj)) {
17747
+ const jsonKey = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
17748
+ const childPath = nodePath ? `${nodePath}.${jsonKey}` : `.${jsonKey}`;
17749
+ obj[key] = resolveConfigEnvVars(obj[key], configFileName, childPath);
17750
+ }
17751
+ return obj;
17752
+ }
17753
+ return node;
17754
+ }
17697
17755
  function readConfig(workspaceRoot) {
17698
17756
  const filePath = configPath(workspaceRoot);
17699
17757
  const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } };
17758
+ let raw;
17759
+ try {
17760
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17761
+ } catch {
17762
+ return defaults;
17763
+ }
17764
+ loadDotEnv(workspaceRoot);
17765
+ try {
17766
+ resolveConfigEnvVars(raw, CONFIG_FILENAME);
17767
+ } catch (err) {
17768
+ const msg = err instanceof Error ? err.message : String(err);
17769
+ process.stderr.write(`
17770
+ Configuration error: ${msg}
17771
+
17772
+ Set the missing environment variable before starting the server.
17773
+
17774
+ `);
17775
+ process.exit(1);
17776
+ }
17700
17777
  try {
17701
- const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17702
17778
  const isV1 = raw.version === 1 || !raw.version && !(typeof raw.boards === "object" && raw.boards !== null && !Array.isArray(raw.boards));
17703
17779
  if (isV1) {
17704
17780
  const v2 = migrateConfigV1ToV2(raw);
@@ -21302,7 +21378,7 @@ function resolveAuthIdentityPlugin(ref) {
21302
21378
  if (ref.provider === "rbac")
21303
21379
  return RBAC_IDENTITY_PLUGIN;
21304
21380
  const packageName = AUTH_PROVIDER_ALIASES.get(ref.provider) ?? ref.provider;
21305
- return loadExternalAuthIdentityPlugin(packageName, ref.provider);
21381
+ return loadExternalAuthIdentityPlugin(packageName, ref.provider, ref.options);
21306
21382
  }
21307
21383
  function resolveAuthPolicyPlugin(ref) {
21308
21384
  if (ref.provider === "noop")
@@ -21528,8 +21604,13 @@ function selectAuthPolicyPlugin(mod, providerId) {
21528
21604
  return direct;
21529
21605
  return null;
21530
21606
  }
21531
- function loadExternalAuthIdentityPlugin(packageName, providerId) {
21607
+ function loadExternalAuthIdentityPlugin(packageName, providerId, options) {
21532
21608
  const mod = loadExternalModule(packageName);
21609
+ if (options !== void 0 && typeof mod.createAuthIdentityPlugin === "function") {
21610
+ const created = mod.createAuthIdentityPlugin(options);
21611
+ if (isValidAuthIdentityPlugin(created, providerId))
21612
+ return created;
21613
+ }
21533
21614
  const plugin = selectAuthIdentityPlugin(mod, providerId);
21534
21615
  if (!plugin) {
21535
21616
  throw new Error(
@@ -24532,29 +24613,10 @@ async function updateCard(ctx, { cardId, updates, boardId }) {
24532
24613
  return card;
24533
24614
  }
24534
24615
  async function triggerAction(ctx, { cardId, action, boardId }) {
24535
- const config = readConfig(ctx.workspaceRoot);
24536
- const { actionWebhookUrl } = config;
24537
- if (!actionWebhookUrl) {
24538
- throw new Error("No action webhook URL configured. Set actionWebhookUrl in .kanban.json");
24539
- }
24540
24616
  const card = await getCard(ctx, { cardId, boardId });
24541
24617
  if (!card)
24542
24618
  throw new Error(`Card not found: ${cardId}`);
24543
24619
  const resolvedBoardId = card.boardId || ctx._resolveBoardId(boardId);
24544
- const payload = {
24545
- action,
24546
- board: resolvedBoardId,
24547
- list: card.status,
24548
- card: sanitizeCard(card)
24549
- };
24550
- const response = await fetch(actionWebhookUrl, {
24551
- method: "POST",
24552
- headers: { "Content-Type": "application/json" },
24553
- body: JSON.stringify(payload)
24554
- });
24555
- if (!response.ok) {
24556
- throw new Error(`Action webhook responded with ${response.status}: ${response.statusText}`);
24557
- }
24558
24620
  await appendActivityLog(ctx, {
24559
24621
  cardId,
24560
24622
  boardId: resolvedBoardId,
@@ -24565,6 +24627,12 @@ async function triggerAction(ctx, { cardId, action, boardId }) {
24565
24627
  }
24566
24628
  }).catch(() => {
24567
24629
  });
24630
+ return {
24631
+ action,
24632
+ board: resolvedBoardId,
24633
+ list: card.status,
24634
+ card: sanitizeCard(card)
24635
+ };
24568
24636
  }
24569
24637
  async function submitForm(ctx, input) {
24570
24638
  const card = await getCard(ctx, { cardId: input.cardId, boardId: input.boardId });
@@ -25981,6 +26049,25 @@ var KanbanSDK = class _KanbanSDK {
25981
26049
  get workspaceRoot() {
25982
26050
  return path14.dirname(this.kanbanDir);
25983
26051
  }
26052
+ /**
26053
+ * Returns a cloned read-only snapshot of the current workspace config.
26054
+ *
26055
+ * The returned snapshot is created from a fresh config read and deep-cloned
26056
+ * before being returned, so callers receive an isolated view of the current
26057
+ * `.kanban.json` state rather than a live mutable runtime object. Mutating the
26058
+ * returned snapshot does not update persisted config or affect this SDK instance.
26059
+ *
26060
+ * @returns A cloned read-only snapshot of the current {@link KanbanConfig}.
26061
+ *
26062
+ * @example
26063
+ * ```ts
26064
+ * const config = sdk.getConfigSnapshot()
26065
+ * console.log(config.defaultBoard)
26066
+ * ```
26067
+ */
26068
+ getConfigSnapshot() {
26069
+ return structuredClone(readConfig(this.workspaceRoot));
26070
+ }
25984
26071
  // --- Board resolution helpers ---
25985
26072
  /** @internal */
25986
26073
  _resolveBoardId(boardId) {
@@ -26447,21 +26534,17 @@ var KanbanSDK = class _KanbanSDK {
26447
26534
  return result;
26448
26535
  }
26449
26536
  /**
26450
- * Triggers a named action for a card by POSTing to the global `actionWebhookUrl`
26451
- * configured in `.kanban.json`.
26537
+ * Triggers a named action for a card.
26452
26538
  *
26453
- * The payload sent to the webhook is:
26454
- * ```json
26455
- * { "action": "retry", "board": "default", "list": "in-progress", "card": { ...sanitizedCard } }
26456
- * ```
26539
+ * Validates the card, appends an activity log entry, and emits the
26540
+ * `card.action.triggered` after-event so registered webhooks receive
26541
+ * the action payload automatically.
26457
26542
  *
26458
26543
  * @param cardId - The ID of the card to trigger the action for.
26459
26544
  * @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
26460
26545
  * @param boardId - Optional board ID. Defaults to the workspace's default board.
26461
- * @returns A promise resolving when the webhook responds with 2xx.
26462
- * @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
26546
+ * @returns A promise that resolves when the action has been processed.
26463
26547
  * @throws {Error} If the card is not found.
26464
- * @throws {Error} If the webhook responds with a non-2xx status.
26465
26548
  *
26466
26549
  * @example
26467
26550
  * ```ts
@@ -26471,7 +26554,8 @@ var KanbanSDK = class _KanbanSDK {
26471
26554
  */
26472
26555
  async triggerAction(cardId, action, boardId) {
26473
26556
  const mergedInput = await this._runBeforeEvent("card.action.trigger", { cardId, action, boardId }, void 0, boardId);
26474
- return triggerAction(this, mergedInput);
26557
+ const payload = await triggerAction(this, mergedInput);
26558
+ this._runAfterEvent("card.action.triggered", payload, void 0, payload.board);
26475
26559
  }
26476
26560
  /**
26477
26561
  * Moves a card to a different status column and/or position within that column.
@@ -27519,6 +27603,7 @@ var AFTER_ENTRIES = [
27519
27603
  { event: "board.updated", resource: "board", label: "Board updated" },
27520
27604
  { event: "board.deleted", resource: "board", label: "Board deleted" },
27521
27605
  { event: "board.action", resource: "board", label: "Board action triggered" },
27606
+ { event: "card.action.triggered", resource: "card", label: "Card action triggered" },
27522
27607
  { event: "board.log.added", resource: "board", label: "Board log added" },
27523
27608
  { event: "board.log.cleared", resource: "board", label: "Board log cleared" },
27524
27609
  // card logs
@@ -17653,11 +17653,87 @@ function migrateConfigV1ToV2(raw) {
17653
17653
  }
17654
17654
  return v2;
17655
17655
  }
17656
+ function loadDotEnv(dir) {
17657
+ const envPath = path.join(dir, ".env");
17658
+ let content;
17659
+ try {
17660
+ content = fs.readFileSync(envPath, "utf-8");
17661
+ } catch {
17662
+ return;
17663
+ }
17664
+ for (const line of content.split("\n")) {
17665
+ const trimmed = line.trim();
17666
+ if (!trimmed || trimmed.startsWith("#"))
17667
+ continue;
17668
+ const eqIdx = trimmed.indexOf("=");
17669
+ if (eqIdx < 1)
17670
+ continue;
17671
+ const key = trimmed.slice(0, eqIdx).trim();
17672
+ let val = trimmed.slice(eqIdx + 1).trim();
17673
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
17674
+ val = val.slice(1, -1);
17675
+ }
17676
+ if (process.env[key] === void 0) {
17677
+ process.env[key] = val;
17678
+ }
17679
+ }
17680
+ }
17681
+ function resolveConfigEnvVars(node, configFileName, nodePath = "") {
17682
+ const isFormDefaultDataPath = /^\.forms\.(?:[^.]+|"[^"]+")\.data(?:$|[.\[])/.test(nodePath);
17683
+ if (isFormDefaultDataPath) {
17684
+ return node;
17685
+ }
17686
+ if (typeof node === "string") {
17687
+ return node.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
17688
+ const envValue = process.env[varName];
17689
+ if (envValue === void 0) {
17690
+ throw new Error(
17691
+ `missing ${varName} in ${configFileName}: ${nodePath} "${node}"`
17692
+ );
17693
+ }
17694
+ return envValue;
17695
+ });
17696
+ }
17697
+ if (Array.isArray(node)) {
17698
+ for (let i = 0; i < node.length; i++) {
17699
+ node[i] = resolveConfigEnvVars(node[i], configFileName, `${nodePath}[${i}]`);
17700
+ }
17701
+ return node;
17702
+ }
17703
+ if (node !== null && typeof node === "object") {
17704
+ const obj = node;
17705
+ for (const key of Object.keys(obj)) {
17706
+ const jsonKey = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
17707
+ const childPath = nodePath ? `${nodePath}.${jsonKey}` : `.${jsonKey}`;
17708
+ obj[key] = resolveConfigEnvVars(obj[key], configFileName, childPath);
17709
+ }
17710
+ return obj;
17711
+ }
17712
+ return node;
17713
+ }
17656
17714
  function readConfig(workspaceRoot) {
17657
17715
  const filePath = configPath(workspaceRoot);
17658
17716
  const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } };
17717
+ let raw;
17718
+ try {
17719
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17720
+ } catch {
17721
+ return defaults;
17722
+ }
17723
+ loadDotEnv(workspaceRoot);
17724
+ try {
17725
+ resolveConfigEnvVars(raw, CONFIG_FILENAME);
17726
+ } catch (err) {
17727
+ const msg = err instanceof Error ? err.message : String(err);
17728
+ process.stderr.write(`
17729
+ Configuration error: ${msg}
17730
+
17731
+ Set the missing environment variable before starting the server.
17732
+
17733
+ `);
17734
+ process.exit(1);
17735
+ }
17659
17736
  try {
17660
- const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17661
17737
  const isV1 = raw.version === 1 || !raw.version && !(typeof raw.boards === "object" && raw.boards !== null && !Array.isArray(raw.boards));
17662
17738
  if (isV1) {
17663
17739
  const v2 = migrateConfigV1ToV2(raw);
@@ -21261,7 +21337,7 @@ function resolveAuthIdentityPlugin(ref) {
21261
21337
  if (ref.provider === "rbac")
21262
21338
  return RBAC_IDENTITY_PLUGIN;
21263
21339
  const packageName = AUTH_PROVIDER_ALIASES.get(ref.provider) ?? ref.provider;
21264
- return loadExternalAuthIdentityPlugin(packageName, ref.provider);
21340
+ return loadExternalAuthIdentityPlugin(packageName, ref.provider, ref.options);
21265
21341
  }
21266
21342
  function resolveAuthPolicyPlugin(ref) {
21267
21343
  if (ref.provider === "noop")
@@ -21487,8 +21563,13 @@ function selectAuthPolicyPlugin(mod, providerId) {
21487
21563
  return direct;
21488
21564
  return null;
21489
21565
  }
21490
- function loadExternalAuthIdentityPlugin(packageName, providerId) {
21566
+ function loadExternalAuthIdentityPlugin(packageName, providerId, options) {
21491
21567
  const mod = loadExternalModule(packageName);
21568
+ if (options !== void 0 && typeof mod.createAuthIdentityPlugin === "function") {
21569
+ const created = mod.createAuthIdentityPlugin(options);
21570
+ if (isValidAuthIdentityPlugin(created, providerId))
21571
+ return created;
21572
+ }
21492
21573
  const plugin = selectAuthIdentityPlugin(mod, providerId);
21493
21574
  if (!plugin) {
21494
21575
  throw new Error(
@@ -24491,29 +24572,10 @@ async function updateCard(ctx, { cardId, updates, boardId }) {
24491
24572
  return card;
24492
24573
  }
24493
24574
  async function triggerAction(ctx, { cardId, action, boardId }) {
24494
- const config = readConfig(ctx.workspaceRoot);
24495
- const { actionWebhookUrl } = config;
24496
- if (!actionWebhookUrl) {
24497
- throw new Error("No action webhook URL configured. Set actionWebhookUrl in .kanban.json");
24498
- }
24499
24575
  const card = await getCard(ctx, { cardId, boardId });
24500
24576
  if (!card)
24501
24577
  throw new Error(`Card not found: ${cardId}`);
24502
24578
  const resolvedBoardId = card.boardId || ctx._resolveBoardId(boardId);
24503
- const payload = {
24504
- action,
24505
- board: resolvedBoardId,
24506
- list: card.status,
24507
- card: sanitizeCard(card)
24508
- };
24509
- const response = await fetch(actionWebhookUrl, {
24510
- method: "POST",
24511
- headers: { "Content-Type": "application/json" },
24512
- body: JSON.stringify(payload)
24513
- });
24514
- if (!response.ok) {
24515
- throw new Error(`Action webhook responded with ${response.status}: ${response.statusText}`);
24516
- }
24517
24579
  await appendActivityLog(ctx, {
24518
24580
  cardId,
24519
24581
  boardId: resolvedBoardId,
@@ -24524,6 +24586,12 @@ async function triggerAction(ctx, { cardId, action, boardId }) {
24524
24586
  }
24525
24587
  }).catch(() => {
24526
24588
  });
24589
+ return {
24590
+ action,
24591
+ board: resolvedBoardId,
24592
+ list: card.status,
24593
+ card: sanitizeCard(card)
24594
+ };
24527
24595
  }
24528
24596
  async function submitForm(ctx, input) {
24529
24597
  const card = await getCard(ctx, { cardId: input.cardId, boardId: input.boardId });
@@ -25940,6 +26008,25 @@ var KanbanSDK = class _KanbanSDK {
25940
26008
  get workspaceRoot() {
25941
26009
  return path14.dirname(this.kanbanDir);
25942
26010
  }
26011
+ /**
26012
+ * Returns a cloned read-only snapshot of the current workspace config.
26013
+ *
26014
+ * The returned snapshot is created from a fresh config read and deep-cloned
26015
+ * before being returned, so callers receive an isolated view of the current
26016
+ * `.kanban.json` state rather than a live mutable runtime object. Mutating the
26017
+ * returned snapshot does not update persisted config or affect this SDK instance.
26018
+ *
26019
+ * @returns A cloned read-only snapshot of the current {@link KanbanConfig}.
26020
+ *
26021
+ * @example
26022
+ * ```ts
26023
+ * const config = sdk.getConfigSnapshot()
26024
+ * console.log(config.defaultBoard)
26025
+ * ```
26026
+ */
26027
+ getConfigSnapshot() {
26028
+ return structuredClone(readConfig(this.workspaceRoot));
26029
+ }
25943
26030
  // --- Board resolution helpers ---
25944
26031
  /** @internal */
25945
26032
  _resolveBoardId(boardId) {
@@ -26406,21 +26493,17 @@ var KanbanSDK = class _KanbanSDK {
26406
26493
  return result;
26407
26494
  }
26408
26495
  /**
26409
- * Triggers a named action for a card by POSTing to the global `actionWebhookUrl`
26410
- * configured in `.kanban.json`.
26496
+ * Triggers a named action for a card.
26411
26497
  *
26412
- * The payload sent to the webhook is:
26413
- * ```json
26414
- * { "action": "retry", "board": "default", "list": "in-progress", "card": { ...sanitizedCard } }
26415
- * ```
26498
+ * Validates the card, appends an activity log entry, and emits the
26499
+ * `card.action.triggered` after-event so registered webhooks receive
26500
+ * the action payload automatically.
26416
26501
  *
26417
26502
  * @param cardId - The ID of the card to trigger the action for.
26418
26503
  * @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
26419
26504
  * @param boardId - Optional board ID. Defaults to the workspace's default board.
26420
- * @returns A promise resolving when the webhook responds with 2xx.
26421
- * @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
26505
+ * @returns A promise that resolves when the action has been processed.
26422
26506
  * @throws {Error} If the card is not found.
26423
- * @throws {Error} If the webhook responds with a non-2xx status.
26424
26507
  *
26425
26508
  * @example
26426
26509
  * ```ts
@@ -26430,7 +26513,8 @@ var KanbanSDK = class _KanbanSDK {
26430
26513
  */
26431
26514
  async triggerAction(cardId, action, boardId) {
26432
26515
  const mergedInput = await this._runBeforeEvent("card.action.trigger", { cardId, action, boardId }, void 0, boardId);
26433
- return triggerAction(this, mergedInput);
26516
+ const payload = await triggerAction(this, mergedInput);
26517
+ this._runAfterEvent("card.action.triggered", payload, void 0, payload.board);
26434
26518
  }
26435
26519
  /**
26436
26520
  * Moves a card to a different status column and/or position within that column.
@@ -27478,6 +27562,7 @@ var AFTER_ENTRIES = [
27478
27562
  { event: "board.updated", resource: "board", label: "Board updated" },
27479
27563
  { event: "board.deleted", resource: "board", label: "Board deleted" },
27480
27564
  { event: "board.action", resource: "board", label: "Board action triggered" },
27565
+ { event: "card.action.triggered", resource: "card", label: "Card action triggered" },
27481
27566
  { event: "board.log.added", resource: "board", label: "Board log added" },
27482
27567
  { event: "board.log.cleared", resource: "board", label: "Board log cleared" },
27483
27568
  // card logs