listspace 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sjenkie B.V. (Yellow House Digital)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # listspace
2
+
3
+ The command line for [ListSpace](https://listspace.app), the project board your AI agents keep up to date. People and coding agents use it to read boards, add and move items, work through a board on their own, and undo their own changes. It talks to the ListSpace REST API (`https://api.listspace.app/v1`).
4
+
5
+ Node 20 or later. No runtime dependencies.
6
+
7
+ ## Install
8
+
9
+ Run it without installing:
10
+
11
+ ```bash
12
+ npx listspace --help
13
+ ```
14
+
15
+ Or install it once and use `listspace` everywhere:
16
+
17
+ ```bash
18
+ npm install -g listspace
19
+ ```
20
+
21
+ ## Sign in
22
+
23
+ Create a personal token in ListSpace under **Settings > API**, then save it:
24
+
25
+ ```bash
26
+ npx listspace login --token ls_...
27
+ ```
28
+
29
+ Without `--token`, `login` reads the token from stdin, so it stays out of your shell history:
30
+
31
+ ```bash
32
+ cat token.txt | npx listspace login
33
+ ```
34
+
35
+ The token is saved as `credentials.json` (mode 600) in your OS config directory: `%APPDATA%\listspace` on Windows, `~/Library/Application Support/listspace` on macOS and `~/.config/listspace` elsewhere. `listspace logout` removes it. A read-only token works for the read commands only.
36
+
37
+ ## Commands
38
+
39
+ ```text
40
+ Sign in
41
+ login [--token ls_...] Save a personal token (ListSpace: Settings > API).
42
+ logout Forget the saved token.
43
+ whoami Plan, freedom levels, guidance and boards (alias: account).
44
+
45
+ Read
46
+ boards Your boards with their ids.
47
+ session <board> Start a work session: guidance, board instructions,
48
+ level, open work. Run this first.
49
+ board <board> Lists and items with their ids.
50
+ inbox <board> [--status pending|accepted|rejected|withdrawn|expired|failed|all]
51
+
52
+ Write (the board's freedom level decides what is made and what waits in the Inbox)
53
+ new-board <title> [--description <text>]
54
+ add <board> <list> <title> [--description <markdown>]
55
+ move <item> <list>
56
+ done <item> Move to the board's done list.
57
+ undo <request_id> Take back one of your own calls.
58
+ Writes take --inbox --reason "<why>" to suggest instead of change.
59
+
60
+ Work on your own (needs level 4, Act)
61
+ next <board> [--kinds todo,backlog] [--label <id>] [--no-move] [--ttl <minutes>]
62
+ claim <item> [--ttl <minutes>]
63
+ release <item>
64
+ ```
65
+
66
+ `<board>` is an id or a title. `<list>` is an id, a title or a kind (`todo`, `doing`, `done`, `info`, `capture`, `backlog`). `<item>` is an item id. Run `listspace --help` for every option.
67
+
68
+ Example:
69
+
70
+ ```bash
71
+ npx listspace session "Website"
72
+ npx listspace add "Website" ideas "Try dark mode" --description "Seen in **settings**"
73
+ ```
74
+
75
+ ## Output for agents and scripts
76
+
77
+ Every command takes `--json` and then prints the API's JSON instead of text:
78
+
79
+ ```bash
80
+ npx listspace boards --json
81
+ ```
82
+
83
+ Exit codes: 0 ok, 1 failed, 2 usage error, 3 sign-in needed, 4 rate limited (wait and retry).
84
+
85
+ ## Environment
86
+
87
+ | Variable | What it does |
88
+ |---|---|
89
+ | `LISTSPACE_TOKEN` | A token for this run. It wins over the saved one. |
90
+ | `LISTSPACE_API_URL` | Another API address. Default `https://api.listspace.app/v1`. |
91
+ | `LISTSPACE_CONFIG_DIR` | Where `credentials.json` is kept. |
92
+
93
+ ## What agents may do
94
+
95
+ Each board has a freedom level that decides what an agent may change on its own. Changes that need your approval wait in the board's Inbox, and every change shows who made it. The rules are in [What AI agents can do](https://listspace.app/docs/agents). Agents that support MCP can also connect to `https://mcp.listspace.app` instead of using this CLI ([setup](https://listspace.app/docs/mcp)).
96
+
97
+ ## License
98
+
99
+ MIT, Sjenkie B.V. (Yellow House Digital). See `LICENSE`.
@@ -0,0 +1,976 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/main.ts
4
+ import { createInterface } from "node:readline/promises";
5
+
6
+ // src/cli.ts
7
+ import { parseArgs } from "node:util";
8
+
9
+ // src/decode.ts
10
+ var DecodeError = class extends Error {
11
+ constructor(path2, expected) {
12
+ super(`Unexpected response from ListSpace: ${path2} should be ${expected}.`);
13
+ this.path = path2;
14
+ this.expected = expected;
15
+ this.name = "DecodeError";
16
+ }
17
+ };
18
+ function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ var string = (value, path2) => {
22
+ if (typeof value !== "string") throw new DecodeError(path2, "a string");
23
+ return value;
24
+ };
25
+ var number = (value, path2) => {
26
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new DecodeError(path2, "a number");
27
+ return value;
28
+ };
29
+ var boolean = (value, path2) => {
30
+ if (typeof value !== "boolean") throw new DecodeError(path2, "true or false");
31
+ return value;
32
+ };
33
+ function literal(...allowed) {
34
+ return (value, path2) => {
35
+ const match = allowed.find((candidate) => candidate === value);
36
+ if (match === void 0) throw new DecodeError(path2, `one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}`);
37
+ return match;
38
+ };
39
+ }
40
+ function nullable(inner) {
41
+ return (value, path2) => value === null ? null : inner(value, path2);
42
+ }
43
+ function withDefault(inner, fallback) {
44
+ return (value, path2) => value === void 0 ? fallback : inner(value, path2);
45
+ }
46
+ function array(inner) {
47
+ return (value, path2) => {
48
+ if (!Array.isArray(value)) throw new DecodeError(path2, "a list");
49
+ return value.map((entry, index) => inner(entry, `${path2}[${index}]`));
50
+ };
51
+ }
52
+ function object(shape) {
53
+ return (value, path2) => {
54
+ if (!isRecord(value)) throw new DecodeError(path2, "an object");
55
+ const out = {};
56
+ for (const [key, decode] of Object.entries(shape)) {
57
+ out[key] = decode(value[key], `${path2}.${key}`);
58
+ }
59
+ return out;
60
+ };
61
+ }
62
+ function union(pick, a, b) {
63
+ return (value, path2) => {
64
+ if (!isRecord(value)) throw new DecodeError(path2, "an object");
65
+ return pick(value) === "a" ? a(value, path2) : b(value, path2);
66
+ };
67
+ }
68
+
69
+ // src/schema.ts
70
+ var LIST_KINDS = ["info", "capture", "backlog", "todo", "doing", "done"];
71
+ var listKind = nullable(literal(...LIST_KINDS));
72
+ var level = object({
73
+ effective: literal(1, 2, 3, 4, 5),
74
+ name: string
75
+ });
76
+ var account = object({
77
+ guidance: nullable(string),
78
+ default_level: object({ effective: literal(1, 2, 3, 4, 5), name: string, limited_by_plan: boolean }),
79
+ plan: object({ name: string, max_agent_level: number }),
80
+ boards: array(
81
+ object({
82
+ id: string,
83
+ title: string,
84
+ url: string,
85
+ level,
86
+ follows_account: boolean,
87
+ inbox_pending: number,
88
+ has_instructions: boolean
89
+ })
90
+ )
91
+ });
92
+ var boardSummary = object({ id: string, title: string, url: string, archived: boolean });
93
+ var cardBrief = object({ id: string, title: string, due_date: nullable(string), done: boolean });
94
+ var boardDetail = object({
95
+ id: string,
96
+ title: string,
97
+ url: string,
98
+ guidance: withDefault(nullable(string), null),
99
+ instructions: withDefault(nullable(string), null),
100
+ level: withDefault(nullable(level), null),
101
+ lists: array(object({ id: string, title: string, kind: listKind, cards: array(cardBrief) })),
102
+ truncated: withDefault(boolean, false)
103
+ });
104
+ var itemBrief = object({
105
+ id: string,
106
+ title: string,
107
+ list_title: string,
108
+ due_date: nullable(string),
109
+ url: string
110
+ });
111
+ var session = object({
112
+ guidance: nullable(string),
113
+ board: object({ id: string, title: string, url: string }),
114
+ instructions: nullable(string),
115
+ level,
116
+ lists: array(object({ id: string, title: string, kind: listKind, open_items: number })),
117
+ work: array(itemBrief),
118
+ captured: array(itemBrief),
119
+ recently_done: array(itemBrief),
120
+ due_soon: array(itemBrief),
121
+ today: string,
122
+ truncated: boolean
123
+ });
124
+ var item = object({ id: string, board_id: string, list_id: string, title: string, url: string });
125
+ var applied = object({
126
+ outcome: literal("applied"),
127
+ level,
128
+ undo: nullable(object({ request_id: string }))
129
+ });
130
+ var suggested = object({
131
+ outcome: literal("suggested"),
132
+ level,
133
+ message: string,
134
+ suggestion: object({ id: string, status: string })
135
+ });
136
+ var writeOutcome = union(
137
+ (value) => value.outcome === "suggested" ? "b" : "a",
138
+ applied,
139
+ suggested
140
+ );
141
+ var createdBoard = object({
142
+ id: string,
143
+ title: string,
144
+ url: string,
145
+ instructions: nullable(string),
146
+ level,
147
+ lists: array(object({ id: string, title: string, kind: listKind })),
148
+ undo: nullable(object({ request_id: string }))
149
+ });
150
+ var inbox = object({
151
+ suggestions: array(
152
+ object({
153
+ id: string,
154
+ action: string,
155
+ item_title: nullable(string),
156
+ list_title: nullable(string),
157
+ reason: nullable(string),
158
+ agent_name: string,
159
+ status: string,
160
+ stale: boolean,
161
+ created_at: string,
162
+ expires_at: string
163
+ })
164
+ )
165
+ });
166
+ var undoResult = object({
167
+ outcome: literal("undone", "already_undone"),
168
+ undone: array(string),
169
+ level: nullable(level)
170
+ });
171
+ var claimedBy = object({ agent_name: nullable(string), expires_at: string });
172
+ var taken = object({
173
+ outcome: literal("taken"),
174
+ item_id: string,
175
+ claimed_by: claimedBy,
176
+ message: string,
177
+ level
178
+ });
179
+ var claimHeld = object({ outcome: literal("claimed", "renewed"), item_id: string, expires_at: string, level });
180
+ var claimResult = union(
181
+ (value) => value.outcome === "taken" ? "b" : "a",
182
+ claimHeld,
183
+ taken
184
+ );
185
+ var released = object({ outcome: literal("released", "not_claimed"), item_id: string, level });
186
+ var releaseResult = union(
187
+ (value) => value.outcome === "taken" ? "b" : "a",
188
+ released,
189
+ taken
190
+ );
191
+ var nextClaimed = object({
192
+ outcome: literal("claimed"),
193
+ item: object({ id: string, title: string, list_id: string, url: string }),
194
+ expires_at: string,
195
+ moved_to: nullable(object({ list_id: string, list_title: string })),
196
+ note: nullable(string),
197
+ level,
198
+ undo: nullable(object({ request_id: string }))
199
+ });
200
+ var nextEmpty = object({ outcome: literal("empty"), message: string, level });
201
+ var nextResult = union(
202
+ (value) => value.outcome === "empty" ? "b" : "a",
203
+ nextClaimed,
204
+ nextEmpty
205
+ );
206
+ function page(entry) {
207
+ return object({ data: array(entry), next_cursor: nullable(string) });
208
+ }
209
+ var errorBody = object({ error: object({ code: string, message: string }) });
210
+
211
+ // src/version.ts
212
+ var VERSION = "0.1.0";
213
+
214
+ // src/api.ts
215
+ var DEFAULT_API_URL = "https://api.listspace.app/v1";
216
+ var TOKEN_PATTERN = /^ls_[A-Za-z0-9_-]{43}$/;
217
+ function parseToken(raw) {
218
+ const trimmed = raw.trim();
219
+ return TOKEN_PATTERN.test(trimmed) ? trimmed : null;
220
+ }
221
+ var CliFailure = class extends Error {
222
+ constructor(error) {
223
+ super(error.message);
224
+ this.error = error;
225
+ this.name = "CliFailure";
226
+ }
227
+ };
228
+ var fail = (error) => {
229
+ throw new CliFailure(error);
230
+ };
231
+ var LOGIN_HINT = "Run `listspace login` with a personal token (ListSpace: Settings > API), or set LISTSPACE_TOKEN.";
232
+ var ApiClient = class {
233
+ constructor(options) {
234
+ this.options = options;
235
+ }
236
+ /** Sends one request and decodes `data` from the answer. */
237
+ async data(method, path2, decoder, init = {}) {
238
+ const json = await this.send(method, path2, init);
239
+ if (!isRecord(json) || !("data" in json)) return fail({ kind: "unexpected", message: "Unexpected response from ListSpace: no data." });
240
+ return { value: decodeOrFail(decoder, json.data, "data"), raw: json.data };
241
+ }
242
+ /** Sends one request and decodes the whole body (list endpoints with next_cursor). */
243
+ async body(method, path2, decoder, init = {}) {
244
+ const json = await this.send(method, path2, init);
245
+ return { value: decodeOrFail(decoder, json, "body"), raw: json };
246
+ }
247
+ async send(method, path2, init) {
248
+ const url = new URL(this.options.baseUrl.replace(/\/+$/, "") + path2);
249
+ for (const [key, value] of Object.entries(init.query ?? {})) {
250
+ if (value !== void 0) url.searchParams.set(key, String(value));
251
+ }
252
+ const headers = {
253
+ Authorization: `Bearer ${this.options.token}`,
254
+ Accept: "application/json",
255
+ "User-Agent": `listspace-cli/${VERSION}`
256
+ };
257
+ if (init.body !== void 0) headers["Content-Type"] = "application/json";
258
+ let response;
259
+ try {
260
+ response = await this.options.fetch(
261
+ url.toString(),
262
+ init.body === void 0 ? { method, headers } : { method, headers, body: JSON.stringify(init.body) }
263
+ );
264
+ } catch (cause) {
265
+ const reason = cause instanceof Error ? cause.message : String(cause);
266
+ return fail({ kind: "network", message: `Could not reach ListSpace at ${url.origin}: ${reason}` });
267
+ }
268
+ const text = await response.text();
269
+ let json = null;
270
+ if (text !== "") {
271
+ try {
272
+ json = JSON.parse(text);
273
+ } catch {
274
+ if (response.ok) return fail({ kind: "unexpected", message: `ListSpace answered ${response.status} with something that is not JSON.` });
275
+ }
276
+ }
277
+ if (response.ok) return json;
278
+ return fail(errorFor(response, json));
279
+ }
280
+ };
281
+ function decodeOrFail(decoder, value, path2) {
282
+ try {
283
+ return decoder(value, path2);
284
+ } catch (error) {
285
+ if (error instanceof DecodeError) return fail({ kind: "unexpected", message: error.message });
286
+ throw error;
287
+ }
288
+ }
289
+ function errorFor(response, json) {
290
+ let code2 = "unknown";
291
+ let message = `ListSpace answered ${response.status}.`;
292
+ try {
293
+ const parsed = errorBody(json, "body");
294
+ code2 = parsed.error.code;
295
+ message = parsed.error.message;
296
+ } catch {
297
+ }
298
+ if (response.status === 401) return { kind: "auth", message: `${message} ${LOGIN_HINT}` };
299
+ if (response.status === 429) {
300
+ const header = response.headers.get("Retry-After");
301
+ const seconds = header !== null && /^\d+$/.test(header) ? Number(header) : null;
302
+ const wait = seconds === null ? "Wait a minute" : `Wait ${seconds} seconds`;
303
+ return { kind: "rate_limited", message: `${message} ${wait} before trying again.`, retryAfterSeconds: seconds };
304
+ }
305
+ if (response.status === 403 && code2 === "insufficient_scope") {
306
+ return { kind: "api", status: 403, code: code2, message: `${message} This token is read only; create a read and write token in Settings > API.` };
307
+ }
308
+ return { kind: "api", status: response.status, code: code2, message };
309
+ }
310
+
311
+ // src/format.ts
312
+ var levelText = (level2) => `level ${level2.effective} (${level2.name})`;
313
+ function block(title, text) {
314
+ if (text === null || text.trim() === "") return [];
315
+ return [`${title}:`, ...text.trim().split("\n").map((line) => ` ${line}`)];
316
+ }
317
+ function items(title, list, showList) {
318
+ if (list.length === 0) return [];
319
+ return [
320
+ `${title}:`,
321
+ ...list.map((entry) => {
322
+ const where = showList ? ` [${entry.list_title}]` : "";
323
+ const due = entry.due_date === null ? "" : ` (due ${entry.due_date})`;
324
+ return ` - ${entry.title}${where}${due} ${entry.id}`;
325
+ })
326
+ ];
327
+ }
328
+ function formatAccount(account2) {
329
+ const lines = [
330
+ `Plan: ${account2.plan.name}`,
331
+ `Default ${levelText(account2.default_level)}${account2.default_level.limited_by_plan ? ", limited by the plan" : ""}`,
332
+ ...block("Guidance for agents", account2.guidance),
333
+ `Boards (${account2.boards.length}):`,
334
+ ...account2.boards.map((board) => {
335
+ const notes = [levelText(board.level)];
336
+ if (board.inbox_pending > 0) notes.push(`${board.inbox_pending} in Inbox`);
337
+ if (board.has_instructions) notes.push("has instructions");
338
+ return ` - ${board.title} ${board.id} (${notes.join(", ")})`;
339
+ })
340
+ ];
341
+ return lines.join("\n");
342
+ }
343
+ function formatBoards(boards) {
344
+ if (boards.length === 0) return 'No boards yet. Create one with `listspace new-board "<title>"`.';
345
+ return boards.map((board) => `${board.title} ${board.id}`).join("\n");
346
+ }
347
+ function formatSession(session2) {
348
+ const lists = session2.lists.map((list) => `${list.title}${list.kind === null ? "" : ` (${list.kind})`}: ${list.open_items}`);
349
+ return [
350
+ `${session2.board.title} ${session2.board.id}`,
351
+ `${session2.board.url}`,
352
+ `Agent ${levelText(session2.level)}. Today is ${session2.today}.`,
353
+ ...block("Guidance for agents (read first)", session2.guidance),
354
+ ...block("Board instructions (follow these)", session2.instructions),
355
+ `Lists: ${lists.join(", ")}`,
356
+ ...items("Work", session2.work, true),
357
+ ...items("Captured", session2.captured, true),
358
+ ...items("Due soon", session2.due_soon, true),
359
+ ...items("Recently done", session2.recently_done, false),
360
+ ...session2.truncated ? ["(Some sections were cut; run `listspace board <id>` for everything.)"] : []
361
+ ].join("\n");
362
+ }
363
+ function formatBoard(board) {
364
+ return [
365
+ `${board.title} ${board.id}`,
366
+ board.url,
367
+ ...board.level === null ? [] : [`Agent ${levelText(board.level)}.`],
368
+ ...block("Guidance for agents (read first)", board.guidance),
369
+ ...block("Board instructions (follow these)", board.instructions),
370
+ ...board.lists.flatMap((list) => [
371
+ `${list.title}${list.kind === null ? "" : ` (${list.kind})`} ${list.id}`,
372
+ ...list.cards.map((card) => ` - ${card.title}${card.due_date === null ? "" : ` (due ${card.due_date}${card.done ? ", done" : ""})`} ${card.id}`)
373
+ ]),
374
+ ...board.truncated ? ["(Only the first 1,000 items are shown.)"] : []
375
+ ].join("\n");
376
+ }
377
+ function formatWrite(outcome, done) {
378
+ if (outcome.outcome === "suggested") {
379
+ return [
380
+ `Suggested, not made: ${outcome.message}`,
381
+ `It waits in the board's Inbox for the user to accept or reject (${levelText(outcome.level)}). Do not send it again.`,
382
+ `Suggestion ${outcome.suggestion.id}`
383
+ ].join("\n");
384
+ }
385
+ const undo = outcome.undo === null ? [] : [`Undo: listspace undo ${outcome.undo.request_id}`];
386
+ return [`${done} (${levelText(outcome.level)}).`, ...undo].join("\n");
387
+ }
388
+ function formatCreatedBoard(board) {
389
+ return [
390
+ `Created board "${board.title}" ${board.id}`,
391
+ board.url,
392
+ `Lists: ${board.lists.map((list) => list.title).join(", ")}`,
393
+ ...block("Board instructions (follow these)", board.instructions),
394
+ ...board.undo === null ? [] : [`Undo: listspace undo ${board.undo.request_id}`]
395
+ ].join("\n");
396
+ }
397
+ function formatInbox(inbox2) {
398
+ if (inbox2.suggestions.length === 0) return "The Inbox is empty.";
399
+ return inbox2.suggestions.map((suggestion) => {
400
+ const target = [suggestion.item_title, suggestion.list_title].filter((part) => part !== null).join(" -> ");
401
+ const lines = [`${suggestion.status}: ${suggestion.action}${target === "" ? "" : ` ${target}`} ${suggestion.id}`];
402
+ if (suggestion.reason !== null) lines.push(` Reason: ${suggestion.reason}`);
403
+ lines.push(` By ${suggestion.agent_name}, ${suggestion.created_at.slice(0, 10)}, expires ${suggestion.expires_at.slice(0, 10)}${suggestion.stale ? ", item changed since" : ""}`);
404
+ return lines.join("\n");
405
+ }).join("\n");
406
+ }
407
+ function formatUndo(result) {
408
+ if (result.outcome === "already_undone") return "Nothing left to undo: it was undone already.";
409
+ return `Undone (${result.undone.length} change${result.undone.length === 1 ? "" : "s"}).`;
410
+ }
411
+ function formatTaken(result) {
412
+ return [
413
+ `Taken: ${result.claimed_by.agent_name ?? "another app or assistant"} is working on this item until ${result.claimed_by.expires_at}.`,
414
+ result.message
415
+ ].join("\n");
416
+ }
417
+ function formatNext(result) {
418
+ switch (result.outcome) {
419
+ case "empty":
420
+ return `Nothing left: ${result.message}`;
421
+ case "claimed": {
422
+ const lines = [
423
+ `Claimed "${result.item.title}" ${result.item.id}`,
424
+ result.item.url,
425
+ result.moved_to === null ? "Not moved." : `Moved to ${result.moved_to.list_title}.`,
426
+ `Claim runs out at ${result.expires_at} (${levelText(result.level)}); any write of yours on the item renews it.`
427
+ ];
428
+ if (result.note !== null) lines.push(`Note: ${result.note}`);
429
+ if (result.undo !== null) lines.push(`Undo the move: listspace undo ${result.undo.request_id}`);
430
+ lines.push(`When done: listspace release ${result.item.id}`);
431
+ return lines.join("\n");
432
+ }
433
+ default: {
434
+ const exhaustive = result;
435
+ return exhaustive;
436
+ }
437
+ }
438
+ }
439
+ function formatClaim(result) {
440
+ switch (result.outcome) {
441
+ case "claimed":
442
+ case "renewed":
443
+ return `${result.outcome === "claimed" ? "Claimed" : "Renewed"} ${result.item_id} until ${result.expires_at} (${levelText(result.level)}).`;
444
+ case "taken":
445
+ return formatTaken(result);
446
+ default: {
447
+ const exhaustive = result;
448
+ return exhaustive;
449
+ }
450
+ }
451
+ }
452
+ function formatRelease(result) {
453
+ switch (result.outcome) {
454
+ case "released":
455
+ return `Released ${result.item_id}.`;
456
+ case "not_claimed":
457
+ return `${result.item_id} had no claim to release.`;
458
+ case "taken":
459
+ return formatTaken(result);
460
+ default: {
461
+ const exhaustive = result;
462
+ return exhaustive;
463
+ }
464
+ }
465
+ }
466
+
467
+ // src/commands.ts
468
+ var inboxFields = (choice) => {
469
+ switch (choice.kind) {
470
+ case "inbox":
471
+ return { inbox: true, reason: choice.reason };
472
+ case "direct":
473
+ return choice.reason === null ? {} : { reason: choice.reason };
474
+ default: {
475
+ const exhaustive = choice;
476
+ return exhaustive;
477
+ }
478
+ }
479
+ };
480
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
481
+ var path = (id) => encodeURIComponent(id);
482
+ async function allBoards(client) {
483
+ const boards = [];
484
+ const raw = [];
485
+ let cursor;
486
+ for (let pageNumber = 0; pageNumber < 20; pageNumber += 1) {
487
+ const answer = await client.body("GET", "/boards", page(boardSummary), { query: { limit: 100, cursor } });
488
+ boards.push(...answer.value.data);
489
+ if (isRecord(answer.raw) && Array.isArray(answer.raw.data)) raw.push(...answer.raw.data);
490
+ if (answer.value.next_cursor === null) break;
491
+ cursor = answer.value.next_cursor;
492
+ }
493
+ return { boards, raw };
494
+ }
495
+ async function resolveBoardId(client, ref) {
496
+ if (UUID.test(ref)) return ref;
497
+ const { boards } = await allBoards(client);
498
+ const wanted = ref.trim().toLowerCase();
499
+ const exact = boards.filter((board) => board.title.toLowerCase() === wanted);
500
+ const matches = exact.length > 0 ? exact : boards.filter((board) => board.title.toLowerCase().startsWith(wanted));
501
+ const [first, ...others] = matches;
502
+ if (first === void 0) return fail({ kind: "usage", message: `No board called "${ref}". Run \`listspace boards\` to see them.` });
503
+ if (others.length > 0) {
504
+ return fail({ kind: "usage", message: `"${ref}" matches ${matches.length} boards: ${matches.map((b) => `${b.title} (${b.id})`).join(", ")}. Use the id.` });
505
+ }
506
+ return first.id;
507
+ }
508
+ function resolveList(board, ref) {
509
+ const wanted = ref.trim().toLowerCase();
510
+ const byId = board.lists.find((list) => list.id === ref);
511
+ if (byId !== void 0) return byId;
512
+ const byTitle = board.lists.filter((list) => list.title.toLowerCase() === wanted);
513
+ const kind = LIST_KINDS.find((candidate) => candidate === wanted);
514
+ const candidates = byTitle.length > 0 ? byTitle : kind === void 0 ? [] : board.lists.filter((list) => list.kind === kind);
515
+ const [first, ...others] = candidates;
516
+ const names = board.lists.map((list) => list.title).join(", ");
517
+ if (first === void 0) return fail({ kind: "usage", message: `No list "${ref}" on ${board.title}. Its lists: ${names}.` });
518
+ if (others.length > 0) {
519
+ return fail({ kind: "usage", message: `"${ref}" matches ${candidates.length} lists on ${board.title}: ${candidates.map((l) => l.title).join(", ")}. Use the title or id.` });
520
+ }
521
+ return first;
522
+ }
523
+ var getBoard = (client, boardId) => client.data("GET", `/boards/${path(boardId)}`, boardDetail);
524
+ async function accountCommand(client) {
525
+ const answer = await client.data("GET", "/account", account);
526
+ return { raw: answer.raw, text: formatAccount(answer.value) };
527
+ }
528
+ async function boardsCommand(client) {
529
+ const { boards, raw } = await allBoards(client);
530
+ return { raw: { data: raw }, text: formatBoards(boards) };
531
+ }
532
+ async function sessionCommand(client, input) {
533
+ const boardId = await resolveBoardId(client, input.board);
534
+ const answer = await client.data("GET", `/boards/${path(boardId)}/session`, session);
535
+ return { raw: answer.raw, text: formatSession(answer.value) };
536
+ }
537
+ async function boardCommand(client, input) {
538
+ const boardId = await resolveBoardId(client, input.board);
539
+ const answer = await getBoard(client, boardId);
540
+ return { raw: answer.raw, text: formatBoard(answer.value) };
541
+ }
542
+ async function newBoardCommand(client, input) {
543
+ const body = input.description === null ? { title: input.title } : { title: input.title, description: input.description };
544
+ const answer = await client.data("POST", "/boards", createdBoard, { body });
545
+ return { raw: answer.raw, text: formatCreatedBoard(answer.value) };
546
+ }
547
+ async function addCommand(client, input) {
548
+ const boardId = await resolveBoardId(client, input.board);
549
+ const board = (await getBoard(client, boardId)).value;
550
+ const list = resolveList(board, input.list);
551
+ const body = {
552
+ list_id: list.id,
553
+ title: input.title,
554
+ ...input.description === null ? {} : { description: input.description, description_format: "markdown" },
555
+ ...inboxFields(input.choice)
556
+ };
557
+ const answer = await client.data("POST", "/items", writeOutcome, { body });
558
+ return { raw: answer.raw, text: formatWrite(answer.value, `Added "${input.title}" to ${list.title}`) };
559
+ }
560
+ async function moveTo(client, itemId, pickList, choice) {
561
+ const current = (await client.data("GET", `/items/${path(itemId)}`, item)).value;
562
+ const board = (await getBoard(client, current.board_id)).value;
563
+ const list = pickList(board);
564
+ const body = { list_id: list.id, position: "top", ...inboxFields(choice) };
565
+ const answer = await client.data("POST", `/items/${path(itemId)}/move`, writeOutcome, { body });
566
+ return { raw: answer.raw, text: formatWrite(answer.value, `Moved "${current.title}" to ${list.title}`) };
567
+ }
568
+ function moveCommand(client, input) {
569
+ return moveTo(client, input.item, (board) => resolveList(board, input.list), input.choice);
570
+ }
571
+ function doneCommand(client, input) {
572
+ return moveTo(
573
+ client,
574
+ input.item,
575
+ (board) => {
576
+ const doneList = board.lists.find((list) => list.kind === "done");
577
+ if (doneList === void 0) {
578
+ return fail({ kind: "usage", message: `${board.title} has no done list. Use \`listspace move <item> <list>\` instead.` });
579
+ }
580
+ return doneList;
581
+ },
582
+ input.choice
583
+ );
584
+ }
585
+ var INBOX_STATUSES = ["pending", "accepted", "rejected", "withdrawn", "expired", "failed", "all"];
586
+ async function inboxCommand(client, input) {
587
+ const boardId = await resolveBoardId(client, input.board);
588
+ const answer = await client.data("GET", `/boards/${path(boardId)}/inbox`, inbox, { query: { status: input.status } });
589
+ return { raw: answer.raw, text: formatInbox(answer.value) };
590
+ }
591
+ async function undoCommand(client, input) {
592
+ const answer = await client.data("POST", `/activity/${path(input.requestId)}/undo`, undoResult);
593
+ return { raw: answer.raw, text: formatUndo(answer.value) };
594
+ }
595
+ function parseClaimMinutes(raw) {
596
+ if (!/^\d+$/.test(raw.trim())) return null;
597
+ const minutes = Number(raw.trim());
598
+ return minutes >= 5 && minutes <= 1440 ? minutes : null;
599
+ }
600
+ async function nextCommand(client, input) {
601
+ const boardId = await resolveBoardId(client, input.board);
602
+ const body = {
603
+ ...input.kinds.length === 0 ? {} : { kinds: input.kinds },
604
+ ...input.labelIds.length === 0 ? {} : { label_ids: input.labelIds },
605
+ move_to_doing: input.moveToDoing,
606
+ ...input.ttl === null ? {} : { ttl_minutes: input.ttl }
607
+ };
608
+ const answer = await client.data("POST", `/boards/${path(boardId)}/next`, nextResult, { body });
609
+ return { raw: answer.raw, text: formatNext(answer.value) };
610
+ }
611
+ async function claimCommand(client, input) {
612
+ const body = input.ttl === null ? {} : { ttl_minutes: input.ttl };
613
+ const answer = await client.data("POST", `/items/${path(input.item)}/claim`, claimResult, { body });
614
+ return { raw: answer.raw, text: formatClaim(answer.value) };
615
+ }
616
+ async function releaseCommand(client, input) {
617
+ const answer = await client.data("DELETE", `/items/${path(input.item)}/claim`, releaseResult);
618
+ return { raw: answer.raw, text: formatRelease(answer.value) };
619
+ }
620
+
621
+ // src/config.ts
622
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
623
+ import { homedir } from "node:os";
624
+ import { join } from "node:path";
625
+ function configDir(env, platform = process.platform, home = homedir()) {
626
+ const override = env.LISTSPACE_CONFIG_DIR;
627
+ if (override !== void 0 && override !== "") return override;
628
+ if (platform === "win32") return join(env.APPDATA ?? join(home, "AppData", "Roaming"), "listspace");
629
+ if (platform === "darwin") return join(home, "Library", "Application Support", "listspace");
630
+ const xdg = env.XDG_CONFIG_HOME;
631
+ return join(xdg !== void 0 && xdg !== "" ? xdg : join(home, ".config"), "listspace");
632
+ }
633
+ var credentialsPath = (dir) => join(dir, "credentials.json");
634
+ async function loadToken(env, dir) {
635
+ const fromEnv = env.LISTSPACE_TOKEN;
636
+ if (fromEnv !== void 0 && fromEnv !== "") {
637
+ const token2 = parseToken(fromEnv);
638
+ return token2 === null ? { kind: "invalid", where: "LISTSPACE_TOKEN" } : { kind: "env", token: token2 };
639
+ }
640
+ const path2 = credentialsPath(dir);
641
+ let text;
642
+ try {
643
+ text = await readFile(path2, "utf8");
644
+ } catch {
645
+ return { kind: "none" };
646
+ }
647
+ let json;
648
+ try {
649
+ json = JSON.parse(text);
650
+ } catch {
651
+ return { kind: "invalid", where: path2 };
652
+ }
653
+ if (!isRecord(json) || typeof json.token !== "string") return { kind: "invalid", where: path2 };
654
+ const token = parseToken(json.token);
655
+ return token === null ? { kind: "invalid", where: path2 } : { kind: "file", token, path: path2 };
656
+ }
657
+ async function saveToken(dir, token) {
658
+ await mkdir(dir, { recursive: true, mode: 448 });
659
+ const path2 = credentialsPath(dir);
660
+ await writeFile(path2, `${JSON.stringify({ token, saved_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
661
+ `, { mode: 384 });
662
+ await chmod(path2, 384);
663
+ return path2;
664
+ }
665
+ async function removeToken(dir) {
666
+ const path2 = credentialsPath(dir);
667
+ try {
668
+ await readFile(path2);
669
+ } catch {
670
+ return false;
671
+ }
672
+ await rm(path2);
673
+ return true;
674
+ }
675
+
676
+ // src/cli.ts
677
+ var HELP = `listspace ${VERSION}: ListSpace from the command line, for people and coding agents.
678
+
679
+ Usage: listspace <command> [arguments] [--json]
680
+
681
+ Sign in
682
+ login [--token ls_...] Save a personal token (ListSpace: Settings > API).
683
+ Without --token it reads the token from stdin.
684
+ logout Forget the saved token.
685
+ whoami Plan, freedom levels, guidance and boards (alias: account).
686
+
687
+ Read
688
+ boards Your boards with their ids.
689
+ session <board> Start a work session: guidance, board instructions,
690
+ level, open work. Run this first.
691
+ board <board> Lists and items with their ids.
692
+ inbox <board> [--status pending|accepted|rejected|withdrawn|expired|failed|all]
693
+
694
+ Write (the board's freedom level decides what is made and what waits in the Inbox)
695
+ new-board <title> [--description <text>] A board from "Project (for agents)".
696
+ add <board> <list> <title> [--description <markdown>]
697
+ move <item> <list>
698
+ done <item> Move to the board's done list.
699
+ undo <request_id> Take back one of your own calls.
700
+ Writes take --inbox --reason "<why>" to suggest instead of change.
701
+
702
+ Work on your own (needs level 4, Act; other agents leave a claimed item alone)
703
+ next <board> [--kinds todo,backlog] [--label <id>] [--no-move] [--ttl <minutes>]
704
+ Claim the top open item no one is working on and
705
+ move it to Doing. --kinds: lists to take from, in
706
+ order (default todo, then backlog). --label: only
707
+ items with this label (repeat or comma list).
708
+ --no-move: leave it in its list.
709
+ claim <item> [--ttl <minutes>] Claim an item, or renew your claim.
710
+ release <item> Release your claim when you stop (done or not).
711
+ A claim lasts --ttl minutes (5 to 1440, default 120); any write of yours on
712
+ the item renews it.
713
+
714
+ <board> is an id or a title, <list> an id, a title or a kind (todo, doing, done,
715
+ info, capture, backlog), <item> an item id.
716
+
717
+ Options
718
+ --json Print the API's JSON (for agents and scripts).
719
+ --help This help. --version The version.
720
+
721
+ Environment
722
+ LISTSPACE_TOKEN A token for this run (wins over the saved one).
723
+ LISTSPACE_API_URL Default ${DEFAULT_API_URL}
724
+ LISTSPACE_CONFIG_DIR Where credentials.json is kept.
725
+
726
+ Exit codes: 0 ok, 1 failed, 2 usage, 3 sign in needed, 4 rate limited (wait and retry).`;
727
+ var EXIT = {
728
+ usage: 2,
729
+ auth: 3,
730
+ rate_limited: 4,
731
+ api: 1,
732
+ network: 1,
733
+ unexpected: 1
734
+ };
735
+ var OPTIONS = {
736
+ json: { type: "boolean" },
737
+ help: { type: "boolean", short: "h" },
738
+ version: { type: "boolean", short: "v" },
739
+ token: { type: "string" },
740
+ description: { type: "string" },
741
+ inbox: { type: "boolean" },
742
+ reason: { type: "string" },
743
+ status: { type: "string" },
744
+ kinds: { type: "string" },
745
+ label: { type: "string", multiple: true },
746
+ "no-move": { type: "boolean" },
747
+ ttl: { type: "string" }
748
+ };
749
+ var usage = (message) => fail({ kind: "usage", message: `${message} Run \`listspace --help\`.` });
750
+ function positionals(command, given, names) {
751
+ if (given.length !== names.length) {
752
+ const wanted = names.length === 0 ? "no arguments" : names.map((name) => `<${name}>`).join(" ");
753
+ usage(`\`${command}\` takes ${wanted}${given.length > names.length && names.length > 0 ? " (quote text with spaces)" : ""}.`);
754
+ }
755
+ return [...given];
756
+ }
757
+ function choiceFrom(flags) {
758
+ const reason = flags.reason ?? null;
759
+ if (reason !== null && reason.length > 500) usage("--reason is at most 500 characters.");
760
+ if (flags.inbox === true) {
761
+ if (reason === null || reason.trim() === "") return usage('--inbox needs --reason "<why>": the user reads it when deciding.');
762
+ return { kind: "inbox", reason };
763
+ }
764
+ return { kind: "direct", reason };
765
+ }
766
+ function ttlFrom(flags) {
767
+ if (flags.ttl === void 0) return null;
768
+ const minutes = parseClaimMinutes(flags.ttl);
769
+ if (minutes === null) return usage("--ttl is a whole number of minutes from 5 to 1440.");
770
+ return minutes;
771
+ }
772
+ function kindsFrom(flags) {
773
+ if (flags.kinds === void 0) return [];
774
+ const kinds = [];
775
+ for (const part of flags.kinds.split(",")) {
776
+ const wanted = part.trim().toLowerCase();
777
+ const kind = LIST_KINDS.find((candidate) => candidate === wanted);
778
+ if (kind === void 0) return usage(`--kinds takes a comma list of ${LIST_KINDS.join(", ")}; "${part.trim()}" is not one.`);
779
+ if (!kinds.includes(kind)) kinds.push(kind);
780
+ }
781
+ return kinds;
782
+ }
783
+ var labelsFrom = (flags) => [
784
+ ...new Set((flags.label ?? []).flatMap((value) => value.split(",")).map((id) => id.trim()).filter((id) => id !== ""))
785
+ ];
786
+ async function clientFor(io, dir) {
787
+ const source = await loadToken(io.env, dir);
788
+ switch (source.kind) {
789
+ case "env":
790
+ case "file":
791
+ return new ApiClient({ baseUrl: apiUrl(io.env), token: source.token, fetch: io.fetch });
792
+ case "none":
793
+ return fail({ kind: "auth", message: `Not signed in. ${LOGIN_HINT}` });
794
+ case "invalid":
795
+ return fail({ kind: "auth", message: `The token in ${source.where} is not a ListSpace token (ls_ and 43 characters). ${LOGIN_HINT}` });
796
+ default: {
797
+ const exhaustive = source;
798
+ return exhaustive;
799
+ }
800
+ }
801
+ }
802
+ var apiUrl = (env) => {
803
+ const url = env.LISTSPACE_API_URL;
804
+ return url !== void 0 && url !== "" ? url : DEFAULT_API_URL;
805
+ };
806
+ async function loginMethod(flags, io) {
807
+ if (flags.token !== void 0) return { kind: "token", raw: flags.token };
808
+ const typed = await io.readSecret("Paste a personal token from ListSpace (Settings > API): ");
809
+ if (typed === null || typed.trim() === "") return { kind: "pairing" };
810
+ return { kind: "token", raw: typed };
811
+ }
812
+ async function login(flags, io, dir) {
813
+ const method = await loginMethod(flags, io);
814
+ if (method.kind === "pairing") {
815
+ return usage("No token given. Pass --token ls_... or pipe one in (signing in from the browser comes later).");
816
+ }
817
+ const token = parseToken(method.raw);
818
+ if (token === null) return usage("That is not a ListSpace token: it starts with ls_ and has 43 more characters.");
819
+ const client = new ApiClient({ baseUrl: apiUrl(io.env), token, fetch: io.fetch });
820
+ const answer = await client.data("GET", "/account", account);
821
+ const path2 = await saveToken(dir, token);
822
+ const text = [`Signed in (plan ${answer.value.plan.name}, ${answer.value.boards.length} boards). Token saved in ${path2}.`];
823
+ if (io.env.LISTSPACE_TOKEN !== void 0 && io.env.LISTSPACE_TOKEN !== "") text.push("Note: LISTSPACE_TOKEN is set and wins over the saved token.");
824
+ return { raw: { signed_in: true, path: path2 }, text: text.join("\n") };
825
+ }
826
+ async function dispatch(command, args, flags, io, dir) {
827
+ switch (command) {
828
+ case "login":
829
+ positionals(command, args, []);
830
+ return login(flags, io, dir);
831
+ case "logout": {
832
+ positionals(command, args, []);
833
+ const removed = await removeToken(dir);
834
+ return { raw: { signed_out: removed }, text: removed ? "Signed out: the saved token is gone (revoke it in Settings > API if it leaked)." : "No saved token." };
835
+ }
836
+ case "whoami":
837
+ case "account":
838
+ positionals(command, args, []);
839
+ return accountCommand(await clientFor(io, dir));
840
+ case "boards":
841
+ positionals(command, args, []);
842
+ return boardsCommand(await clientFor(io, dir));
843
+ case "session": {
844
+ const [board] = positionals(command, args, ["board"]);
845
+ return sessionCommand(await clientFor(io, dir), { board });
846
+ }
847
+ case "board": {
848
+ const [board] = positionals(command, args, ["board"]);
849
+ return boardCommand(await clientFor(io, dir), { board });
850
+ }
851
+ case "new-board": {
852
+ const [title] = positionals(command, args, ["title"]);
853
+ return newBoardCommand(await clientFor(io, dir), { title, description: flags.description ?? null });
854
+ }
855
+ case "add": {
856
+ const [board, list, title] = positionals(command, args, ["board", "list", "title"]);
857
+ const choice = choiceFrom(flags);
858
+ return addCommand(await clientFor(io, dir), { board, list, title, description: flags.description ?? null, choice });
859
+ }
860
+ case "move": {
861
+ const [item2, list] = positionals(command, args, ["item", "list"]);
862
+ const choice = choiceFrom(flags);
863
+ return moveCommand(await clientFor(io, dir), { item: item2, list, choice });
864
+ }
865
+ case "done": {
866
+ const [item2] = positionals(command, args, ["item"]);
867
+ const choice = choiceFrom(flags);
868
+ return doneCommand(await clientFor(io, dir), { item: item2, choice });
869
+ }
870
+ case "inbox": {
871
+ const [board] = positionals(command, args, ["board"]);
872
+ const wanted = flags.status ?? "pending";
873
+ const status = INBOX_STATUSES.find((candidate) => candidate === wanted);
874
+ if (status === void 0) return usage(`--status is one of ${INBOX_STATUSES.join(", ")}.`);
875
+ return inboxCommand(await clientFor(io, dir), { board, status });
876
+ }
877
+ case "next": {
878
+ const [board] = positionals(command, args, ["board"]);
879
+ const input = { board, kinds: kindsFrom(flags), labelIds: labelsFrom(flags), moveToDoing: flags["no-move"] !== true, ttl: ttlFrom(flags) };
880
+ return nextCommand(await clientFor(io, dir), input);
881
+ }
882
+ case "claim": {
883
+ const [item2] = positionals(command, args, ["item"]);
884
+ const ttl = ttlFrom(flags);
885
+ return claimCommand(await clientFor(io, dir), { item: item2, ttl });
886
+ }
887
+ case "release": {
888
+ const [item2] = positionals(command, args, ["item"]);
889
+ return releaseCommand(await clientFor(io, dir), { item: item2 });
890
+ }
891
+ case "undo": {
892
+ const [requestId] = positionals(command, args, ["request_id"]);
893
+ return undoCommand(await clientFor(io, dir), { requestId });
894
+ }
895
+ default:
896
+ return usage(`Unknown command "${command}".`);
897
+ }
898
+ }
899
+ async function run(argv, io) {
900
+ let json = argv.includes("--json");
901
+ try {
902
+ let parsed;
903
+ try {
904
+ parsed = parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true });
905
+ } catch (error) {
906
+ return usage(error instanceof Error ? error.message : String(error));
907
+ }
908
+ const flags = parsed.values;
909
+ json = flags.json === true;
910
+ const [command, ...args] = parsed.positionals;
911
+ if (flags.version === true) {
912
+ io.stdout(json ? JSON.stringify({ version: VERSION }) : VERSION);
913
+ return 0;
914
+ }
915
+ if (command === void 0 || command === "help" || flags.help === true) {
916
+ io.stdout(HELP);
917
+ return 0;
918
+ }
919
+ const dir = io.configDir ?? configDir(io.env);
920
+ const output = await dispatch(command, args, flags, io, dir);
921
+ io.stdout(json ? JSON.stringify(output.raw, null, 2) : output.text);
922
+ return 0;
923
+ } catch (error) {
924
+ if (!(error instanceof CliFailure)) throw error;
925
+ const failure = error.error;
926
+ if (json) {
927
+ io.stdout(JSON.stringify({ error: errorJson(failure) }, null, 2));
928
+ } else {
929
+ io.stderr(`listspace: ${failure.message}`);
930
+ }
931
+ return EXIT[failure.kind];
932
+ }
933
+ }
934
+ function errorJson(error) {
935
+ switch (error.kind) {
936
+ case "api":
937
+ return { kind: error.kind, status: error.status, code: error.code, message: error.message };
938
+ case "rate_limited":
939
+ return { kind: error.kind, message: error.message, retry_after_seconds: error.retryAfterSeconds };
940
+ case "usage":
941
+ case "auth":
942
+ case "network":
943
+ case "unexpected":
944
+ return { kind: error.kind, message: error.message };
945
+ default: {
946
+ const exhaustive = error;
947
+ return exhaustive;
948
+ }
949
+ }
950
+ }
951
+
952
+ // src/main.ts
953
+ async function readSecret(prompt) {
954
+ if (process.stdin.isTTY) {
955
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
956
+ try {
957
+ return await rl.question(prompt);
958
+ } finally {
959
+ rl.close();
960
+ }
961
+ }
962
+ const chunks = [];
963
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
964
+ const text = Buffer.concat(chunks).toString("utf8").trim();
965
+ return text === "" ? null : text;
966
+ }
967
+ var code = await run(process.argv.slice(2), {
968
+ env: process.env,
969
+ fetch: (url, init) => fetch(url, init),
970
+ stdout: (text) => process.stdout.write(`${text}
971
+ `),
972
+ stderr: (text) => process.stderr.write(`${text}
973
+ `),
974
+ readSecret
975
+ });
976
+ process.exitCode = code;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "listspace",
3
+ "version": "0.1.0",
4
+ "description": "ListSpace from the command line: the agentic project memory, for people and coding agents.",
5
+ "keywords": [
6
+ "listspace",
7
+ "cli",
8
+ "ai-agents",
9
+ "project-memory",
10
+ "kanban",
11
+ "boards",
12
+ "mcp",
13
+ "claude",
14
+ "codex",
15
+ "cursor"
16
+ ],
17
+ "homepage": "https://listspace.app/docs/agents",
18
+ "bugs": {
19
+ "url": "https://github.com/listspace/agent/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/listspace/agent.git",
24
+ "directory": "cli"
25
+ },
26
+ "license": "MIT",
27
+ "author": "Sjenkie B.V. (Yellow House Digital)",
28
+ "type": "module",
29
+ "bin": {
30
+ "listspace": "dist/listspace.mjs"
31
+ },
32
+ "files": [
33
+ "dist/listspace.mjs",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "scripts": {
41
+ "build": "esbuild src/main.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/listspace.mjs --banner:js=\"#!/usr/bin/env node\"",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "tsc -p tsconfig.test.json && node --test build/test/api.test.js build/test/cli.test.js build/test/config.test.js build/test/commands.test.js",
44
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "20.19.43",
48
+ "esbuild": "0.25.5",
49
+ "typescript": "5.8.3"
50
+ }
51
+ }