@andypai/agent-kanban 0.8.0 → 0.8.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/README.md CHANGED
@@ -20,7 +20,8 @@ That buys you a few things that are easy to miss at first:
20
20
 
21
21
  ## Documentation
22
22
 
23
- - [`docs/readme.md`](https://github.com/abpai/agent-kanban/blob/main/docs/readme.md) for the documentation index
23
+ - [`docs/INDEX.md`](https://github.com/abpai/agent-kanban/blob/main/docs/INDEX.md) for the documentation index
24
+ - [`docs/SPEC_CONTRACT.md`](https://github.com/abpai/agent-kanban/blob/main/docs/SPEC_CONTRACT.md) for the repo proof menu and spec contract
24
25
  - [`docs/workflow.md`](https://github.com/abpai/agent-kanban/blob/main/docs/workflow.md) for a common day-to-day workflow
25
26
  - [`docs/mcp.md`](https://github.com/abpai/agent-kanban/blob/main/docs/mcp.md) for the reusable tracker MCP module
26
27
  - [`docs/providers/linear.md`](https://github.com/abpai/agent-kanban/blob/main/docs/providers/linear.md) for Linear provider details
@@ -271,14 +272,18 @@ compatibility. To require authentication, set a token via `--token` or the
271
272
  accepts `?token=<token>` instead.
272
273
  - `/api/health` stays public (liveness probe).
273
274
  - `/api/webhooks/*` are **not** covered by this token — they authenticate with
274
- the provider webhook secret (e.g. `JIRA_WEBHOOK_SECRET`).
275
+ the provider webhook secret when one is configured. Without that secret,
276
+ Linear/Jira webhooks run in local open dev mode and accept unsigned payloads.
275
277
  - The bundled UI picks the token up once from `?token=` / `#token=` and stores
276
278
  it in `localStorage`, so you can open `https://<host>/?token=<token>`.
277
279
 
278
280
  `--tunnel` exposes the dashboard publicly, so it **refuses to start without a
279
- token**. CORS is off by default (same-origin); set `KANBAN_ALLOWED_ORIGIN` (or
280
- `--allowed-origin`) to allow a specific cross-origin browser client. CORS is
281
- origin hygiene, not an auth control the token is the security boundary.
281
+ token**. In Linear or Jira mode it also refuses to start unless the matching
282
+ webhook secret is set, so auth-exempt webhook routes cannot accept unsigned
283
+ public writes. CORS is off by default (same-origin); set
284
+ `KANBAN_ALLOWED_ORIGIN` (or `--allowed-origin`) to allow a specific cross-origin
285
+ browser client. CORS is origin hygiene, not an auth control — the token is the
286
+ security boundary.
282
287
 
283
288
  ### mcp
284
289
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andypai/agent-kanban",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Agent-friendly kanban board CLI. Manage tasks via bash commands, parse structured JSON output.",
5
5
  "homepage": "https://github.com/abpai/agent-kanban#readme",
6
6
  "repository": {
@@ -45,6 +45,8 @@
45
45
  "access": "public"
46
46
  },
47
47
  "scripts": {
48
+ "bootstrap": "bun install && cd ui && bun install",
49
+ "smoke": "bun src/index.ts --help",
48
50
  "dev": "bun --watch src/index.ts",
49
51
  "start": "bun src/index.ts",
50
52
  "build": "bun build ./src/index.ts --target bun --outdir ./dist",
@@ -56,6 +58,10 @@
56
58
  "check": "bun run lint && bun run typecheck && bun run ui:typecheck",
57
59
  "test": "bun test",
58
60
  "test:watch": "bun test --watch",
61
+ "knip": "knip",
62
+ "test:pg": "DATABASE_URL=${DATABASE_URL:-postgres://postgres:postgres@localhost:${KANBAN_PG_PORT:-5432}/kanban_test} bun test",
63
+ "pg:up": "docker compose -f docker-compose.postgres.yml up -d --wait",
64
+ "pg:down": "docker compose -f docker-compose.postgres.yml down -v",
59
65
  "seed:test": "bun --env-file=.env.test scripts/seed-test-db.ts",
60
66
  "serve": "bun src/index.ts serve",
61
67
  "ui:dev": "cd ui && bun run dev",
@@ -83,6 +89,7 @@
83
89
  "eslint-config-prettier": "^10.1.8",
84
90
  "eslint-plugin-prettier": "^5.5.4",
85
91
  "husky": "^8.0.3",
92
+ "knip": "^6.24.0",
86
93
  "lint-staged": "^15.2.0",
87
94
  "prettier": "^3.7.4",
88
95
  "typescript": "^5.7.2"
package/src/api.ts CHANGED
@@ -5,7 +5,7 @@ import { parsePositiveInt } from './transport-input'
5
5
  import { success } from './output'
6
6
  import { normalizeCreateTaskInput } from './use-cases'
7
7
 
8
- export type WsEvent =
8
+ type WsEvent =
9
9
  | { type: 'task:upsert'; task: Task; columnId: string }
10
10
  | { type: 'task:delete'; id: string }
11
11
  // Fallback when a mutation has no precise event; the UI does a full refresh.
package/src/mcp/index.ts CHANGED
@@ -1,13 +1,5 @@
1
1
  export { createTrackerCore } from './core'
2
2
  export { createTrackerMcpServer } from './server'
3
- export { TrackerMcpError, type TrackerMcpErrorCode } from './errors'
4
- export type { TrackerCore } from './core'
5
- export type {
6
- TrackerMcpAuthResolver,
7
- TrackerMcpHooks,
8
- TrackerMcpPolicy,
9
- TrackerMcpServer,
10
- TrackerMcpTool,
11
- TrackerMcpToolHandlerContext,
12
- } from './types'
3
+ export { TrackerMcpError } from './errors'
4
+ export type { TrackerMcpPolicy, TrackerMcpTool } from './types'
13
5
  export { defaultTools } from './server'
package/src/mcp/types.ts CHANGED
@@ -61,7 +61,7 @@ export interface TrackerMcpHooks<TScope> {
61
61
  }): Promise<void> | void
62
62
  }
63
63
 
64
- export interface TrackerMcpToolHandlerContext<TScope, TArgs = Record<string, unknown>> {
64
+ interface TrackerMcpToolHandlerContext<TScope, TArgs = Record<string, unknown>> {
65
65
  scope: TScope
66
66
  args: TArgs
67
67
  request?: Request
@@ -14,7 +14,7 @@ import {
14
14
  const PRIORITY_RANK: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }
15
15
 
16
16
  /** A column plus how many tasks currently sit in it. */
17
- export interface MetricsColumnCount extends ClassifiableColumn {
17
+ interface MetricsColumnCount extends ClassifiableColumn {
18
18
  count: number
19
19
  }
20
20
 
@@ -17,18 +17,18 @@ export interface AdfDocument {
17
17
  content: AdfBlockNode[]
18
18
  }
19
19
 
20
- export interface AdfMark {
20
+ interface AdfMark {
21
21
  type: string
22
22
  attrs?: Record<string, unknown>
23
23
  }
24
24
 
25
- export interface AdfTextNode {
25
+ interface AdfTextNode {
26
26
  type: 'text'
27
27
  text: string
28
28
  marks?: AdfMark[]
29
29
  }
30
30
 
31
- export interface AdfUnknownInlineNode {
31
+ interface AdfUnknownInlineNode {
32
32
  type: string
33
33
  [key: string]: unknown
34
34
  }
@@ -40,7 +40,7 @@ export interface AdfParagraphNode {
40
40
  content?: AdfInlineNode[]
41
41
  }
42
42
 
43
- export interface AdfListItemNode {
43
+ interface AdfListItemNode {
44
44
  type: 'listItem'
45
45
  content: AdfBlockNode[]
46
46
  }
@@ -50,7 +50,7 @@ export interface AdfBulletListNode {
50
50
  content: AdfListItemNode[]
51
51
  }
52
52
 
53
- export interface AdfOrderedListNode {
53
+ interface AdfOrderedListNode {
54
54
  type: 'orderedList'
55
55
  content: AdfListItemNode[]
56
56
  attrs?: { order?: number }
@@ -62,7 +62,7 @@ export interface AdfCodeBlockNode {
62
62
  content?: AdfInlineNode[]
63
63
  }
64
64
 
65
- export interface AdfHeadingNode {
65
+ interface AdfHeadingNode {
66
66
  type: 'heading'
67
67
  attrs: { level: number }
68
68
  content?: AdfInlineNode[]
@@ -74,12 +74,12 @@ export interface AdfExpandNode {
74
74
  content: AdfBlockNode[]
75
75
  }
76
76
 
77
- export interface AdfUnknownBlockNode {
77
+ interface AdfUnknownBlockNode {
78
78
  type: string
79
79
  [key: string]: unknown
80
80
  }
81
81
 
82
- export type AdfBlockNode =
82
+ type AdfBlockNode =
83
83
  | AdfParagraphNode
84
84
  | AdfBulletListNode
85
85
  | AdfOrderedListNode
@@ -88,9 +88,6 @@ export type AdfBlockNode =
88
88
  | AdfExpandNode
89
89
  | AdfUnknownBlockNode
90
90
 
91
- // Public AdfNode union covers every node shape this module recognizes.
92
- export type AdfNode = AdfDocument | AdfBlockNode | AdfListItemNode | AdfInlineNode
93
-
94
91
  const BULLET_MARKER = /^[-*] (.*)$/
95
92
  const ORDERED_MARKER = /^(\d+)\. (.*)$/
96
93
  // Opening/closing fence: `` ``` `` optionally followed by a language tag with
@@ -168,7 +168,7 @@ export interface JiraIssueType {
168
168
  name: string
169
169
  }
170
170
 
171
- export interface JiraChangelogItem {
171
+ interface JiraChangelogItem {
172
172
  field: string
173
173
  fieldtype?: string
174
174
  fromString?: string | null
@@ -177,7 +177,7 @@ export interface JiraChangelogItem {
177
177
  to?: string | null
178
178
  }
179
179
 
180
- export interface JiraChangelogEntry {
180
+ interface JiraChangelogEntry {
181
181
  id: string
182
182
  author?: { accountId?: string; displayName?: string }
183
183
  created: string
@@ -51,9 +51,9 @@ import { WEBHOOK_SECRET_ENV, webhookSecretFromEnv } from '../tracker-config'
51
51
  import { warnOnce } from './warn-once'
52
52
  import { applyTaskFilters, forEachWithConcurrency, SyncGate, syncStatusFromMeta } from './sync-core'
53
53
 
54
- export const FULL_RECONCILE_INTERVAL_MS = 5 * 60_000
54
+ const FULL_RECONCILE_INTERVAL_MS = 5 * 60_000
55
55
 
56
- export function shouldRunFullReconcile(lastFullSyncAt: string | null, now: number): boolean {
56
+ function shouldRunFullReconcile(lastFullSyncAt: string | null, now: number): boolean {
57
57
  if (!lastFullSyncAt) return true
58
58
  const lastFullSyncAtMs = Date.parse(lastFullSyncAt)
59
59
  if (!Number.isFinite(lastFullSyncAtMs)) return true
@@ -64,7 +64,7 @@ export function shouldRunFullReconcile(lastFullSyncAt: string | null, now: numbe
64
64
  // priorities; the write path looks up the resolved name (case-insensitive)
65
65
  // in the cached `jira_priorities` table, so renames that preserve the default
66
66
  // casing still resolve.
67
- export const CANONICAL_TO_JIRA_DEFAULT: Record<Priority, string> = {
67
+ const CANONICAL_TO_JIRA_DEFAULT: Record<Priority, string> = {
68
68
  urgent: 'Highest',
69
69
  high: 'High',
70
70
  medium: 'Medium',
@@ -316,10 +316,10 @@ export function upsertProjects(
316
316
  }
317
317
 
318
318
  // Bound per-value storage so repeated edits to a long description can't balloon the cache.
319
- // Exported and shared with the Postgres cache (postgres-linear-cache.ts) so both
320
- // backends clamp identically and the truncation marker can't drift between them.
321
- export const ACTIVITY_VALUE_MAX_CHARS = 4096
322
- export const ACTIVITY_TRUNCATION_SUFFIX = '…[truncated]'
319
+ // clampActivityValue is shared with the Postgres cache (postgres-linear-cache.ts)
320
+ // so both backends clamp identically and the truncation marker can't drift.
321
+ const ACTIVITY_VALUE_MAX_CHARS = 4096
322
+ const ACTIVITY_TRUNCATION_SUFFIX = '…[truncated]'
323
323
  const ACTIVITY_VALUE_BUDGET = ACTIVITY_VALUE_MAX_CHARS - ACTIVITY_TRUNCATION_SUFFIX.length
324
324
 
325
325
  export function clampActivityValue(value: string): string {
@@ -694,7 +694,7 @@ export async function resolveLabelIdsForCreate(
694
694
  return resolveLinearLabelIds(inputLabels, await client.listIssueLabels())
695
695
  }
696
696
 
697
- export function resolveLinearLabelIds(
697
+ function resolveLinearLabelIds(
698
698
  inputLabels: string[] | undefined,
699
699
  availableLabels: LinearIssueLabel[],
700
700
  ): string[] | undefined {
@@ -15,7 +15,7 @@ import { parseProviderTeamInfo } from './team-info'
15
15
 
16
16
  export type { LinearActivityRow, LinearStateRow, LinearSyncMeta } from './linear-cache'
17
17
 
18
- export type LinearIssueRow = LinearTaskRow
18
+ type LinearIssueRow = LinearTaskRow
19
19
 
20
20
  /**
21
21
  * Postgres-backed cache/repository for the Linear provider. Mirrors the role of
@@ -1,13 +1,11 @@
1
1
  import { getDbPath } from './db'
2
2
 
3
- export type KanbanStorageMode = 'sqlite' | 'postgres'
4
-
5
- export interface SqliteKanbanStorageConfig {
3
+ interface SqliteKanbanStorageConfig {
6
4
  mode: 'sqlite'
7
5
  sqlitePath: string
8
6
  }
9
7
 
10
- export interface PostgresKanbanStorageConfig {
8
+ interface PostgresKanbanStorageConfig {
11
9
  mode: 'postgres'
12
10
  databaseUrl: string
13
11
  }
@@ -62,14 +62,14 @@ export interface LocalTrackerConfig {
62
62
  syncIntervalMs?: number
63
63
  }
64
64
 
65
- export interface LinearTrackerConfig {
65
+ interface LinearTrackerConfig {
66
66
  provider: 'linear'
67
67
  apiKey: string
68
68
  teamId: string
69
69
  syncIntervalMs?: number
70
70
  }
71
71
 
72
- export interface JiraTrackerConfig {
72
+ interface JiraTrackerConfig {
73
73
  provider: 'jira'
74
74
  baseUrl: string
75
75
  email: string