kandown 0.23.0 → 0.25.0
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/CHANGELOG.md +21 -0
- package/README.md +7 -4
- package/bin/kandown.js +178 -40
- package/dist/index.html +155 -139
- package/package.json +1 -1
- package/templates/kandown.json +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0 — 2026-07-20 — "Task Deep Links"
|
|
4
|
+
|
|
5
|
+
- **Added**: **Shareable task deep links** — opening a task drawer now updates the browser URL to a copy-pasteable route such as `/210?p=kandown`, where numeric task segments are normalized to Kandown task ids like `t210`. This makes it possible to send a direct link to a specific task instead of asking someone to find it manually on the board.
|
|
6
|
+
- **Added**: **Direct URL hydration for task drawers** — pasted links are parsed on web app startup and after browser navigation. Kandown now accepts the canonical `/210?p=kandown` format plus tolerant alternatives such as `/t210?p=kandown`, `?task=210`, and `?p=kandown/210`, then opens the matching drawer once the project is loaded.
|
|
7
|
+
- **Added**: **Browser history support for task navigation** — drawer open and close actions now synchronize with `pushState`/`replaceState`, and Kandown listens for `popstate` so browser Back and Forward switch between board-only and task-focused URLs instead of leaving stale drawer state behind.
|
|
8
|
+
- **Added**: **Copy URL action in the task drawer** — the opened task header now includes a lightweight Copy URL control that writes the absolute task link to the clipboard, with a toast fallback that displays the URL if clipboard access is denied.
|
|
9
|
+
- **Fixed**: **Daemon routing for deep links** — the local web daemon now serves the single-page app for safe one-segment task paths like `/210`, preventing direct task URLs from returning `404 Not found` before React has a chance to hydrate and open the drawer.
|
|
10
|
+
- **Changed**: **Project URL handling** — board/project URLs now go through shared URL helpers so project-only navigation keeps `/?p=<project>` while task navigation cleanly adds or removes the task path segment.
|
|
11
|
+
- **Changed**: **Board housekeeping** — completed Kandown task files were archived or removed from the active task set, new backlog tasks were added for upcoming archive/subtask/opened-task redesign work, and local board config/runtime ignore files were updated to match the current project workflow.
|
|
12
|
+
|
|
13
|
+
## 0.24.0 — 2026-07-20 — "Work Output Configurator"
|
|
14
|
+
|
|
15
|
+
- **Added**: **`kandown work` Output Configurator** — added a new Settings panel under Agent configuration that lets each project control the markdown emitted by `kandown work`. Users can enable or disable the base rules, project instructions, and live board digest blocks without editing generated files by hand.
|
|
16
|
+
- **Added**: **Raw Template Mode for Agent Context** — added an advanced raw editor for the complete `kandown work` output with `{{baseRules}}`, `{{projectInstructions}}`, and `{{boardDigest}}` variables. This gives power users full control over the final prompt while keeping the source of truth in `.kandown/kandown.json`.
|
|
17
|
+
- **Added**: **Token-Efficient Concise Rules Mode** — added a compact Kandown rules preset for `kandown work` that keeps the essential task-management contract while dramatically reducing prompt size for cost-sensitive or context-limited agents.
|
|
18
|
+
- **Added**: **Estimated Token Counter and Output Preview** — the Settings configurator now previews the generated `kandown work` markdown and displays an approximate token estimate so users can tune the output before handing it to an agent.
|
|
19
|
+
- **Added**: **Project Instructions Editor** — added authenticated web/server helpers and UI editing for `.kandown/instructions.md`, keeping custom project behavior inside `.kandown/` instead of polluting project-root agent files.
|
|
20
|
+
- **Changed**: **Project-Scoped Work Output Storage** — added `agent.workOutput` to `.kandown/kandown.json` and the default template config, including section toggles, section ordering, digest detail toggles, base-rule verbosity, and raw template content.
|
|
21
|
+
- **Changed**: **Configurable Board Digest Detail** — the generated board digest can now hide column counts, task lists, priorities, assignees, blocked-by annotations, and next-actionable-task output independently to minimize tokens when desired.
|
|
22
|
+
- **Fixed**: **Safe `.kandown/AGENT_KANDOWN.md` Healing** — Kandown now recreates `.kandown/AGENT_KANDOWN.md` when missing and overwrites malformed or stale generated copies from the package template, while keeping all generated reference docs inside `.kandown/` only.
|
|
23
|
+
|
|
3
24
|
## 0.23.0 — 2026-07-20 — "Theme Switcher & Auto Update"
|
|
4
25
|
|
|
5
26
|
- **Added**: **Animated Theme Switcher** — added a shadcn-style three-option theme switcher under `src/components/ui/theme-switcher-1.tsx` with system, light, and dark choices, lucide icons, and Motion layout animation.
|
package/README.md
CHANGED
|
@@ -131,11 +131,14 @@ kandown commit -m "tasks: add auth refactor"
|
|
|
131
131
|
Running `kandown work` prints, as plain markdown on stdout:
|
|
132
132
|
|
|
133
133
|
1. **The agent rules** — always fresh, served straight from the installed CLI version instead of a copy that goes stale the moment the package updates.
|
|
134
|
-
2. **
|
|
135
|
-
3. **
|
|
136
|
-
4. **A live board digest** — column counts, tasks per column with blocked-by annotations, and a computed **"next actionable task"** (closest to done, unblocked, highest priority) — so the agent gets its context in the same call.
|
|
134
|
+
2. **Project instructions** (optional) — `.kandown/instructions.md`, this project only (stack quirks, "always use pnpm", commit message language, token-efficient agent preferences, etc).
|
|
135
|
+
3. **A live board digest** — column counts, tasks per column with blocked-by annotations, and a computed **"next actionable task"** (closest to done, unblocked, highest priority) — so the agent gets its context in the same call.
|
|
137
136
|
|
|
138
|
-
|
|
137
|
+
The Settings page includes an **Agent → kandown work output** configurator. You can toggle each generated block, switch the base rules to a concise token-efficient mode, hide detailed digest fields, see an estimated token count, or use **Raw template** mode to control the complete output with `{{baseRules}}`, `{{projectInstructions}}`, and `{{boardDigest}}` variables.
|
|
138
|
+
|
|
139
|
+
Kandown keeps generated agent reference docs inside `.kandown/` only. If `.kandown/AGENT_KANDOWN.md` is missing, malformed, or an outdated generated copy, the CLI recreates it from the installed package template. Custom behavior belongs in `.kandown/instructions.md` and `.kandown/kandown.json`, not in project-root agent files.
|
|
140
|
+
|
|
141
|
+
This removes the drift problem of a rules block frozen into every project's `AGENTS.md` at init time, keeps the injected footprint to one line, and lets you layer project instructions without touching the agent file at all.
|
|
139
142
|
|
|
140
143
|
> **Upgrading from before v0.18.0?** `kandown shell <cmd>` was removed (no alias) — the commands are now top-level: `kandown list/show/create/move/assign/commit`. Existing projects keep their old `AGENTS.md`/`CLAUDE.md` block until you re-run `kandown init`. The CLI prints a one-time notice about this the next time you run an interactive command after updating — see [Environment variables](#environment-variables) if you want to silence version-check output entirely.
|
|
141
144
|
|
package/bin/kandown.js
CHANGED
|
@@ -869,6 +869,7 @@ function ensureKandownDir(rawArgs) {
|
|
|
869
869
|
} else if (migration.skipped) {
|
|
870
870
|
info('Both .kandown/tasks/ and ./tasks/ have files — leaving both in place');
|
|
871
871
|
}
|
|
872
|
+
syncKandownAgentDoc(kandownDir);
|
|
872
873
|
return { kandownDir, alreadyExisted: true };
|
|
873
874
|
}
|
|
874
875
|
|
|
@@ -894,13 +895,13 @@ function doInit(args, cwd, kandownPath, kandownDir) {
|
|
|
894
895
|
copyFileSync(htmlSrc, htmlDest);
|
|
895
896
|
success('kandown.html');
|
|
896
897
|
|
|
897
|
-
// 📖
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
898
|
+
// 📖 Keep the generated agent reference inside `.kandown/` only. It is safe
|
|
899
|
+
// to recreate/overwrite because user-specific behavior belongs in
|
|
900
|
+
// `.kandown/instructions.md` or the `agent.workOutput` config, not in this
|
|
901
|
+
// generated package reference.
|
|
902
|
+
syncKandownAgentDoc(kandownDir);
|
|
903
|
+
success('AGENT_KANDOWN.md');
|
|
904
|
+
|
|
904
905
|
const templatesDir = join(PKG_ROOT, 'templates');
|
|
905
906
|
if (!existsSync(join(kandownDir, 'README.md'))) {
|
|
906
907
|
copyFileSync(join(templatesDir, 'README.md'), join(kandownDir, 'README.md'));
|
|
@@ -1557,6 +1558,93 @@ function priorityRank(p) {
|
|
|
1557
1558
|
return Object.prototype.hasOwnProperty.call(PRIORITY_RANK, p) ? PRIORITY_RANK[p] : 4;
|
|
1558
1559
|
}
|
|
1559
1560
|
|
|
1561
|
+
const WORK_OUTPUT_SECTION_IDS = ['baseRules', 'projectInstructions', 'boardDigest'];
|
|
1562
|
+
const CONCISE_AGENT_RULES = `# Kandown agent rules — concise
|
|
1563
|
+
|
|
1564
|
+
- Task state lives in project-root \`tasks/*.md\`; do not maintain a separate board index.
|
|
1565
|
+
- Before work, read the relevant task file and keep it updated while you progress.
|
|
1566
|
+
- Move tasks by editing frontmatter \`status:\`; complete work by setting \`status: Done\` and adding a markdown \`report:\` summary.
|
|
1567
|
+
- Update subtasks in-place: \`- [ ]\` → \`- [x]\`, with a short \`report:\` line for meaningful progress.
|
|
1568
|
+
- Board columns live in \`.kandown/kandown.json\` under \`board.columns\`; project instructions live in \`.kandown/instructions.md\`.
|
|
1569
|
+
- Never put Kandown task data inside \`.kandown/\`; tasks belong in \`./tasks/\`.`;
|
|
1570
|
+
const DEFAULT_WORK_OUTPUT = {
|
|
1571
|
+
mode: 'blocks',
|
|
1572
|
+
includeBaseRules: true,
|
|
1573
|
+
baseRulesMode: 'full',
|
|
1574
|
+
includeProjectInstructions: true,
|
|
1575
|
+
includeBoardDigest: true,
|
|
1576
|
+
sectionOrder: WORK_OUTPUT_SECTION_IDS,
|
|
1577
|
+
rawTemplate: '{{baseRules}}\n\n---\n\n{{projectInstructions}}\n\n---\n\n{{boardDigest}}',
|
|
1578
|
+
boardDigest: {
|
|
1579
|
+
showColumnCounts: true,
|
|
1580
|
+
showTasks: true,
|
|
1581
|
+
showPriority: true,
|
|
1582
|
+
showAssignee: true,
|
|
1583
|
+
showBlockedBy: true,
|
|
1584
|
+
showNextActionable: true,
|
|
1585
|
+
},
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
function safePlainObject(value) {
|
|
1589
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function normalizeWorkOutputConfig(config) {
|
|
1593
|
+
const raw = safePlainObject(safePlainObject(config?.agent).workOutput);
|
|
1594
|
+
const digest = safePlainObject(raw.boardDigest);
|
|
1595
|
+
const sectionOrder = Array.isArray(raw.sectionOrder)
|
|
1596
|
+
? raw.sectionOrder.filter(id => WORK_OUTPUT_SECTION_IDS.includes(id))
|
|
1597
|
+
: DEFAULT_WORK_OUTPUT.sectionOrder;
|
|
1598
|
+
const order = sectionOrder.length > 0 ? sectionOrder : DEFAULT_WORK_OUTPUT.sectionOrder;
|
|
1599
|
+
return {
|
|
1600
|
+
...DEFAULT_WORK_OUTPUT,
|
|
1601
|
+
...raw,
|
|
1602
|
+
mode: raw.mode === 'raw' ? 'raw' : 'blocks',
|
|
1603
|
+
baseRulesMode: raw.baseRulesMode === 'concise' ? 'concise' : 'full',
|
|
1604
|
+
sectionOrder: order,
|
|
1605
|
+
rawTemplate: typeof raw.rawTemplate === 'string' && raw.rawTemplate.trim()
|
|
1606
|
+
? raw.rawTemplate
|
|
1607
|
+
: DEFAULT_WORK_OUTPUT.rawTemplate,
|
|
1608
|
+
boardDigest: { ...DEFAULT_WORK_OUTPUT.boardDigest, ...digest },
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function readBaseAgentRules(mode = 'full') {
|
|
1613
|
+
if (mode === 'concise') return CONCISE_AGENT_RULES;
|
|
1614
|
+
try {
|
|
1615
|
+
return readFileSync(join(PKG_ROOT, 'templates', 'AGENT_KANDOWN.md'), 'utf8').trim();
|
|
1616
|
+
} catch (e) {
|
|
1617
|
+
warn(`Could not read base rules (${e.message})`);
|
|
1618
|
+
return '';
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function syncKandownAgentDoc(kandownDir) {
|
|
1623
|
+
const source = join(PKG_ROOT, 'templates', 'AGENT_KANDOWN.md');
|
|
1624
|
+
const target = join(kandownDir, 'AGENT_KANDOWN.md');
|
|
1625
|
+
if (!existsSync(source)) return false;
|
|
1626
|
+
const expected = readFileSync(source, 'utf8');
|
|
1627
|
+
const existing = existsSync(target) ? readFileSync(target, 'utf8') : null;
|
|
1628
|
+
const isMissing = existing === null;
|
|
1629
|
+
const isMalformed = existing !== null && !existing.includes('# Kandown') && !existing.includes('## The System');
|
|
1630
|
+
const isStaleGeneratedDoc = existing !== null && existing.includes('# Kandown') && existing !== expected;
|
|
1631
|
+
|
|
1632
|
+
if (isMissing || isMalformed || isStaleGeneratedDoc) {
|
|
1633
|
+
atomicWriteFileSync(target, expected.endsWith('\n') ? expected : `${expected}\n`);
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function applyWorkRawTemplate(template, blocks) {
|
|
1640
|
+
return template
|
|
1641
|
+
.replaceAll('{{baseRules}}', blocks.baseRules)
|
|
1642
|
+
.replaceAll('{{projectInstructions}}', blocks.projectInstructions)
|
|
1643
|
+
.replaceAll('{{boardDigest}}', blocks.boardDigest)
|
|
1644
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1645
|
+
.trim();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1560
1648
|
/**
|
|
1561
1649
|
* 📖 Builds the "Current board" section: column counts, tasks per column
|
|
1562
1650
|
* (with blocked-by annotations), and a "Next actionable task" pick — the
|
|
@@ -1565,7 +1653,8 @@ function priorityRank(p) {
|
|
|
1565
1653
|
* `depends_on`, tie-broken by priority. Mirrors the same gate logic used by
|
|
1566
1654
|
* `move`/the TUI/the web store.
|
|
1567
1655
|
*/
|
|
1568
|
-
function buildBoardDigest(kandownDir) {
|
|
1656
|
+
function buildBoardDigest(kandownDir, options = DEFAULT_WORK_OUTPUT.boardDigest) {
|
|
1657
|
+
const digestOptions = { ...DEFAULT_WORK_OUTPUT.boardDigest, ...safePlainObject(options) };
|
|
1569
1658
|
const config = readKandownConfig(kandownDir);
|
|
1570
1659
|
const columns = (config && config.board && Array.isArray(config.board.columns) && config.board.columns.length > 0)
|
|
1571
1660
|
? config.board.columns
|
|
@@ -1608,17 +1697,21 @@ function buildBoardDigest(kandownDir) {
|
|
|
1608
1697
|
}
|
|
1609
1698
|
|
|
1610
1699
|
const lines = ['## Current board', ''];
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1700
|
+
if (digestOptions.showColumnCounts) {
|
|
1701
|
+
lines.push(`**Columns:** ${columns.map(col => `${col} (${(byColumn.get(col) || []).length})`).join(' · ')}`);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
if (digestOptions.showTasks) {
|
|
1705
|
+
for (const col of columns) {
|
|
1706
|
+
const tasks = (byColumn.get(col) || []).sort((a, b) => priorityRank(a.fm.priority) - priorityRank(b.fm.priority));
|
|
1707
|
+
if (tasks.length === 0) continue;
|
|
1708
|
+
lines.push('', `### ${col}`);
|
|
1709
|
+
for (const t of tasks) {
|
|
1710
|
+
const pri = digestOptions.showPriority && t.fm.priority ? `[${t.fm.priority}] ` : '';
|
|
1711
|
+
const assignee = digestOptions.showAssignee && t.fm.assignee ? ` (@${t.fm.assignee})` : '';
|
|
1712
|
+
const blockedStr = digestOptions.showBlockedBy && t.blocked.length > 0 ? ` ⛔ blocked by ${t.blocked.join(', ')}` : '';
|
|
1713
|
+
lines.push(`- ${t.id} ${pri}${t.fm.title || '(untitled)'}${assignee}${blockedStr}`);
|
|
1714
|
+
}
|
|
1622
1715
|
}
|
|
1623
1716
|
}
|
|
1624
1717
|
|
|
@@ -1635,30 +1728,23 @@ function buildBoardDigest(kandownDir) {
|
|
|
1635
1728
|
return priorityRank(a.fm.priority) - priorityRank(b.fm.priority);
|
|
1636
1729
|
})[0];
|
|
1637
1730
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1731
|
+
if (digestOptions.showNextActionable) {
|
|
1732
|
+
lines.push('', '### Next actionable task');
|
|
1733
|
+
lines.push(next
|
|
1734
|
+
? `→ **${next.id}** — ${next.fm.title || '(untitled)'} (${next.fm.priority || 'no priority'}, ${next.fm.status || columns[0]})`
|
|
1735
|
+
: 'None — every task is done, archived, or blocked.');
|
|
1736
|
+
}
|
|
1642
1737
|
|
|
1643
1738
|
return lines.join('\n');
|
|
1644
1739
|
}
|
|
1645
1740
|
|
|
1646
1741
|
async function cmdWork(rawArgs) {
|
|
1647
1742
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1743
|
+
const config = readKandownConfig(kandownDir);
|
|
1744
|
+
const workOutput = normalizeWorkOutputConfig(config);
|
|
1745
|
+
syncKandownAgentDoc(kandownDir);
|
|
1648
1746
|
|
|
1649
|
-
|
|
1650
|
-
try {
|
|
1651
|
-
baseRules = readFileSync(join(PKG_ROOT, 'templates', 'AGENT_KANDOWN.md'), 'utf8').trim();
|
|
1652
|
-
} catch (e) {
|
|
1653
|
-
warn(`Could not read base rules (${e.message})`);
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
let globalInstructions = '';
|
|
1657
|
-
const globalPath = join(homedir(), '.kandown', 'instructions.md');
|
|
1658
|
-
if (existsSync(globalPath)) {
|
|
1659
|
-
try { globalInstructions = readFileSync(globalPath, 'utf8').trim(); }
|
|
1660
|
-
catch (e) { warn(`Could not read global instructions (${e.message})`); }
|
|
1661
|
-
}
|
|
1747
|
+
const baseRules = readBaseAgentRules(workOutput.baseRulesMode);
|
|
1662
1748
|
|
|
1663
1749
|
let projectInstructions = '';
|
|
1664
1750
|
const projectPath = join(kandownDir, 'instructions.md');
|
|
@@ -1667,10 +1753,23 @@ async function cmdWork(rawArgs) {
|
|
|
1667
1753
|
catch (e) { warn(`Could not read project instructions (${e.message})`); }
|
|
1668
1754
|
}
|
|
1669
1755
|
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1756
|
+
const blocks = {
|
|
1757
|
+
baseRules,
|
|
1758
|
+
projectInstructions: projectInstructions ? `## Project-specific instructions\n\n${projectInstructions}` : '',
|
|
1759
|
+
boardDigest: buildBoardDigest(kandownDir, workOutput.boardDigest),
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
if (workOutput.mode === 'raw') {
|
|
1763
|
+
out(applyWorkRawTemplate(workOutput.rawTemplate, blocks));
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const sections = [];
|
|
1768
|
+
for (const sectionId of workOutput.sectionOrder) {
|
|
1769
|
+
if (sectionId === 'baseRules' && workOutput.includeBaseRules) sections.push(blocks.baseRules);
|
|
1770
|
+
if (sectionId === 'projectInstructions' && workOutput.includeProjectInstructions) sections.push(blocks.projectInstructions);
|
|
1771
|
+
if (sectionId === 'boardDigest' && workOutput.includeBoardDigest) sections.push(blocks.boardDigest);
|
|
1772
|
+
}
|
|
1674
1773
|
|
|
1675
1774
|
out(sections.filter(Boolean).join('\n\n---\n\n'));
|
|
1676
1775
|
}
|
|
@@ -2232,6 +2331,30 @@ function putConfig(req, res, kandownDir) {
|
|
|
2232
2331
|
});
|
|
2233
2332
|
}
|
|
2234
2333
|
|
|
2334
|
+
function getInstructions(res, kandownDir) {
|
|
2335
|
+
const instructionsPath = join(kandownDir, 'instructions.md');
|
|
2336
|
+
if (!existsSync(instructionsPath)) {
|
|
2337
|
+
writeText(res, 200, '');
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
try {
|
|
2341
|
+
writeText(res, 200, readFileSync(instructionsPath, 'utf8'));
|
|
2342
|
+
} catch (e) {
|
|
2343
|
+
writeText(res, 500, `Failed to read instructions: ${e.message}`);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function putInstructions(req, res, kandownDir) {
|
|
2348
|
+
readBody(req).then(body => {
|
|
2349
|
+
const instructionsPath = join(kandownDir, 'instructions.md');
|
|
2350
|
+
const normalized = body.trim() ? body.replace(/\s+$/, '') + '\n' : '';
|
|
2351
|
+
atomicWriteFileSync(instructionsPath, normalized);
|
|
2352
|
+
writeJson(res, 200, { ok: true });
|
|
2353
|
+
}).catch(e => {
|
|
2354
|
+
writeJson(res, e.statusCode || 500, { error: `Failed to write instructions: ${e.message}` });
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2235
2358
|
function getBoard(res, kandownDir) {
|
|
2236
2359
|
const boardPath = join(kandownDir, 'board.md');
|
|
2237
2360
|
if (!existsSync(boardPath)) {
|
|
@@ -2505,6 +2628,11 @@ function handleApi(req, res, url, kandownDir) {
|
|
|
2505
2628
|
if (req.method === 'PUT') return putConfig(req, res, kandownDir);
|
|
2506
2629
|
}
|
|
2507
2630
|
|
|
2631
|
+
if (resource === 'instructions') {
|
|
2632
|
+
if (req.method === 'GET') return getInstructions(res, kandownDir);
|
|
2633
|
+
if (req.method === 'PUT') return putInstructions(req, res, kandownDir);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2508
2636
|
if (resource === 'board') {
|
|
2509
2637
|
if (req.method === 'GET') return getBoard(res, kandownDir);
|
|
2510
2638
|
if (req.method === 'PUT') return putBoard(req, res, kandownDir);
|
|
@@ -2598,6 +2726,15 @@ function serveStaticAsset(req, res, pathname) {
|
|
|
2598
2726
|
return false;
|
|
2599
2727
|
}
|
|
2600
2728
|
|
|
2729
|
+
function isTaskDeepLinkPath(pathname) {
|
|
2730
|
+
try {
|
|
2731
|
+
const cleaned = decodeURIComponent(pathname).replace(/^\/+|\/+$/g, '');
|
|
2732
|
+
return !!cleaned && !cleaned.includes('/') && !cleaned.includes('.') && /^[A-Za-z0-9_-]+$/.test(cleaned);
|
|
2733
|
+
} catch {
|
|
2734
|
+
return false;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2601
2738
|
function createServeServer(kandownDir) {
|
|
2602
2739
|
try {
|
|
2603
2740
|
const tasksDir = getTasksDir(kandownDir);
|
|
@@ -2622,6 +2759,7 @@ function createServeServer(kandownDir) {
|
|
|
2622
2759
|
return handleApi(req, res, requestUrl, kandownDir);
|
|
2623
2760
|
}
|
|
2624
2761
|
if (serveStaticAsset(req, res, requestUrl.pathname)) return;
|
|
2762
|
+
if (req.method === 'GET' && isTaskDeepLinkPath(requestUrl.pathname)) return serveApp(res, kandownDir);
|
|
2625
2763
|
return writeText(res, 404, 'Not found');
|
|
2626
2764
|
});
|
|
2627
2765
|
}
|