@evanbobo/colco4dsh 0.0.0-stage → 0.1.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/lib/tasks.mjs ADDED
@@ -0,0 +1,158 @@
1
+ // The My Tasks board's side of the plugin: what the browser may ask for, the colco command that asks it, and how
2
+ // colco's reply is shaped before the page sees it. Pure functions, so the route stays a thin wrapper and every rule
3
+ // here is testable without a process or a socket.
4
+ import { field } from "./format.mjs";
5
+ import { safeLink } from "./dashboard.mjs";
6
+
7
+ /** Tasks per page. The plugin sets it; the browser cannot. */
8
+ export const BOARD_PAGE_SIZE = 25;
9
+
10
+ /** Highest page the board asks for; the same cap the Colco server enforces. */
11
+ export const MAX_BOARD_PAGE = 10000;
12
+
13
+ /** Longest task title and project name kept; the server allows 200 and 80 characters. */
14
+ const TITLE_MAX = 200;
15
+ const PROJECT_NAME_MAX = 80;
16
+
17
+ /** Most project choices kept for the filter. */
18
+ const PROJECT_CHOICES_MAX = 200;
19
+
20
+ /** The statuses of a task in board order, as the Colco server lists them: work in flight first, done last. */
21
+ export const BOARD_STATUSES = ["in_progress", "blocked", "review", "todo", "backlog", "done"];
22
+
23
+ /** Task priorities Colco knows. */
24
+ const PRIORITIES = new Set(["high", "medium", "low"]);
25
+
26
+ /** The only query keys the route accepts. */
27
+ const QUERY_KEYS = new Set(["project", "status", "page"]);
28
+
29
+ /**
30
+ * Parse the query of `GET tasks`. Only a project id, one of the six statuses and a page number pass, written as plain
31
+ * digits or exact words, each at most once. Anything else, including a key that could name a person, is refused, so
32
+ * nothing the browser sends can reach the colco command as an option or as free text.
33
+ * @param {URLSearchParams} search
34
+ * @returns {{ ok: true, value: { project?: number, status?: string, page: number } } | { ok: false, error: string }}
35
+ */
36
+ export function parseBoardQuery(search) {
37
+ for (const key of new Set(search.keys())) {
38
+ if (!QUERY_KEYS.has(key)) return { ok: false, error: `unknown query parameter: ${key.slice(0, 40)}` };
39
+ if (search.getAll(key).length > 1) return { ok: false, error: `${key} may be given only once` };
40
+ }
41
+ const value = { page: 1 };
42
+ if (search.has("project")) {
43
+ const project = search.get("project");
44
+ if (!/^[1-9]\d{0,14}$/.test(project)) return { ok: false, error: "project must be a project id: digits, starting at 1" };
45
+ value.project = Number(project);
46
+ }
47
+ if (search.has("status")) {
48
+ const status = search.get("status");
49
+ if (!BOARD_STATUSES.includes(status)) return { ok: false, error: `status must be one of ${BOARD_STATUSES.join(", ")}` };
50
+ value.status = status;
51
+ }
52
+ if (search.has("page")) {
53
+ const page = search.get("page");
54
+ if (!/^[1-9]\d{0,4}$/.test(page) || Number(page) > MAX_BOARD_PAGE) return { ok: false, error: `page must be a number from 1 to ${MAX_BOARD_PAGE}` };
55
+ value.page = Number(page);
56
+ }
57
+ return { ok: true, value };
58
+ }
59
+
60
+ /**
61
+ * The colco arguments for a parsed query. Each value becomes its own argv element; the page size is the plugin's.
62
+ * @param {{ project?: number, status?: string, page?: number }} query - the value from parseBoardQuery.
63
+ */
64
+ export function boardArgs(query) {
65
+ return [
66
+ "tasks", "--json",
67
+ ...query.project !== undefined ? ["--project-id", String(query.project)] : [],
68
+ ...query.status !== undefined ? ["--status", query.status] : [],
69
+ "--page", String(query.page ?? 1),
70
+ "--page-size", String(BOARD_PAGE_SIZE),
71
+ ];
72
+ }
73
+
74
+ /**
75
+ * A string cut to `max` characters, or undefined when the value is not a non-empty string.
76
+ * @param {unknown} value
77
+ * @param {number} max
78
+ */
79
+ function text(value, max) {
80
+ return typeof value === "string" && value ? value.slice(0, max) : undefined;
81
+ }
82
+
83
+ /**
84
+ * A positive safe integer, or undefined.
85
+ * @param {unknown} value
86
+ */
87
+ function positive(value) {
88
+ return Number.isSafeInteger(value) && value >= 1 ? value : undefined;
89
+ }
90
+
91
+ /**
92
+ * A non-negative safe integer, or the fallback.
93
+ * @param {unknown} value
94
+ * @param {number} fallback
95
+ */
96
+ function count(value, fallback = 0) {
97
+ return Number.isSafeInteger(value) && value >= 0 ? value : fallback;
98
+ }
99
+
100
+ /**
101
+ * One task for the board, or undefined when it lacks a usable id or status. Only these fields are kept, so a
102
+ * description or assignee a future server adds never reaches the browser; the link must be an http(s) URL.
103
+ * @param {Record<string, unknown>} raw - one entry of `tasks` from `colco tasks --json`.
104
+ */
105
+ function boardTask(raw) {
106
+ const id = positive(raw.id);
107
+ if (id === undefined || !BOARD_STATUSES.includes(raw.status)) return undefined;
108
+ return {
109
+ id,
110
+ projectId: positive(raw.projectId),
111
+ projectName: text(raw.projectName, PROJECT_NAME_MAX),
112
+ title: text(raw.title, TITLE_MAX),
113
+ status: raw.status,
114
+ priority: PRIORITIES.has(raw.priority) ? raw.priority : undefined,
115
+ dueDate: typeof raw.dueDate === "string" && /^\d{4}-\d{2}-\d{2}$/.test(raw.dueDate) ? raw.dueDate : null,
116
+ updatedAt: text(raw.updatedAt, 40),
117
+ link: safeLink(raw.link),
118
+ };
119
+ }
120
+
121
+ /**
122
+ * The reply of `GET tasks`: the page of tasks, the paging numbers, one count per status in board order and the
123
+ * projects to filter by, each type-checked and cut. A reply without a `tasks` array fails like any malformed colco
124
+ * reply instead of showing an empty board.
125
+ * @param {Record<string, unknown>} body - `colco tasks --json`.
126
+ */
127
+ export function boardReply(body) {
128
+ const raw = field(body, "tasks", "array", "colco tasks");
129
+ const pageSize = positive(body.pageSize) && body.pageSize <= 100 ? body.pageSize : BOARD_PAGE_SIZE;
130
+ const total = count(body.total);
131
+
132
+ const perStatus = new Map();
133
+ if (Array.isArray(body.counts)) {
134
+ for (const entry of body.counts) {
135
+ if (entry !== null && typeof entry === "object" && BOARD_STATUSES.includes(entry.status) && !perStatus.has(entry.status)) perStatus.set(entry.status, count(entry.count));
136
+ }
137
+ }
138
+
139
+ const projects = [];
140
+ if (Array.isArray(body.projects)) {
141
+ for (const entry of body.projects) {
142
+ if (projects.length === PROJECT_CHOICES_MAX) break;
143
+ const id = entry !== null && typeof entry === "object" ? positive(entry.id) : undefined;
144
+ const name = id === undefined ? undefined : text(entry.name, PROJECT_NAME_MAX);
145
+ if (id !== undefined && name !== undefined) projects.push({ id, name, count: count(entry.count) });
146
+ }
147
+ }
148
+
149
+ return {
150
+ tasks: raw.map(boardTask).filter((task) => task !== undefined).slice(0, pageSize),
151
+ page: positive(body.page) && body.page <= MAX_BOARD_PAGE ? body.page : 1,
152
+ pageSize,
153
+ total,
154
+ pages: Math.max(1, Math.ceil(total / pageSize)),
155
+ counts: BOARD_STATUSES.map((status) => ({ status, count: perStatus.get(status) ?? 0 })),
156
+ projects,
157
+ };
158
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Colco",
4
+ "description": "See your Colco runner, active runs and open decisions in a sidebar tab, set up the runner's rules, and answer decisions from chat."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Colco",
4
+ "description": "\u5728\u4fa7\u8fb9\u680f\u67e5\u770b Colco Runner\u3001\u8fdb\u884c\u4e2d\u7684\u8fd0\u884c\u548c\u5f85\u51b3\u7b56\uff0c\u8bbe\u7f6e Runner \u89c4\u5219\uff0c\u4e5f\u53ef\u4ee5\u5728\u5bf9\u8bdd\u4e2d\u56de\u7b54\u51b3\u7b56\u3002"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@evanbobo/colco4dsh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.4",
4
+ "private": false,
5
+ "description": "DSH plugin for Colco: a Colco page in the left sidebar with your runner, active runs and open decisions, plus chat tools to see and answer them",
6
+ "type": "module",
7
+ "main": "index.mjs",
8
+ "exports": {
9
+ ".": "./index.mjs",
10
+ "./client": "./client.js",
11
+ "./locale/*.json": "./locale/*.json",
12
+ "./icon": "./icon.svg",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "index.mjs",
17
+ "client.js",
18
+ "lib",
19
+ "locale/*.json",
20
+ "icon.svg",
21
+ "cordis.patch.yml",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "platform": "web",
31
+ "inject": [
32
+ "@deepseek-ai/dsh-client-locale",
33
+ "@deepseek-ai/dsh-client-ui-layout",
34
+ "@deepseek-ai/dsh-client-ui-sidebar",
35
+ "@deepseek-ai/dsh-client-ui-renderer"
36
+ ]
37
+ }
38
+ },
39
+ "engines": {
40
+ "dsh": ">=0.2.0 <0.3.0",
41
+ "node": ">=20.3"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org/"
46
+ },
47
+ "peerDependencies": {
48
+ "@deepseek-ai/dsh-tools": ">=0.2.0-0 <0.3.0",
49
+ "@deepseek-ai/schemastery": ">=3.18.0-0 <4.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@deepseek-ai/dsh-tools": "0.2.2-alpha.2",
53
+ "@deepseek-ai/schemastery": "3.18.6-alpha.1"
54
+ },
55
+ "scripts": {
56
+ "test": "bun test"
57
+ },
58
+ "keywords": [
59
+ "colco",
60
+ "deepseek-harness",
61
+ "dsh-plugin"
62
+ ],
63
+ "license": "MIT"
64
+ }