haunter 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +85 -2
  2. package/dist/index.js +245 -19
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -56,6 +56,35 @@ haunter pages read PAGE_ID
56
56
  Lists show page IDs and titles. Reading a page prints its Markdown. Quote search
57
57
  phrases and task titles containing spaces.
58
58
 
59
+ Rename a page without changing its content or location:
60
+
61
+ ```bash
62
+ haunter pages rename PAGE_ID "Updated meeting notes"
63
+ ```
64
+
65
+ Page titles can contain up to 300 characters.
66
+
67
+ ## Search your workspace and list canvases
68
+
69
+ Search across pages, tasks, and canvases in your selected workspace:
70
+
71
+ ```bash
72
+ haunter search "meeting notes"
73
+ haunter search "meeting" --kind task
74
+ haunter canvases list
75
+ ```
76
+
77
+ Search queries must contain 2–200 characters. Search returns up to 20 matches
78
+ per request; use `--limit 1..50` to change that or `--kind all|page|task|canvas`
79
+ to filter the type. Each result shows its type, ID, title, and matching excerpt.
80
+
81
+ When more results are available, the CLI prints a next cursor on stderr.
82
+ Repeat the same query and `--kind` with `--cursor NEXT_CURSOR` to continue.
83
+ With `--json`, the cursor is in the `nextCursor` field of the result.
84
+
85
+ `canvases list` shows IDs and titles of standalone canvases. Canvases embedded
86
+ inside pages are not included.
87
+
59
88
  ## Manage tasks
60
89
 
61
90
  ```bash
@@ -63,6 +92,9 @@ haunter tasks list
63
92
  haunter tasks list --due today
64
93
  haunter tasks add "Review proposal" --due 2026-12-12
65
94
  haunter tasks done TASK_ID
95
+ haunter tasks reopen TASK_ID
96
+ haunter tasks update TASK_ID --title "Review revised proposal" --due 2026-12-13
97
+ haunter tasks update TASK_ID --clear-due
66
98
  ```
67
99
 
68
100
  Task lists default to your open tasks, with a limit of 50. Use
@@ -70,6 +102,56 @@ Task lists default to your open tasks, with a limit of 50. Use
70
102
  `--due today|overdue|upcoming`, and `--limit 1..200` to change the selection.
71
103
  Creating and completing tasks prints the task ID.
72
104
 
105
+ `tasks reopen` marks a completed task as open. `tasks update` changes only the
106
+ fields you supply. Use `--title` to rename a standalone task, `--due YYYY-MM-DD`
107
+ to set its due date, or `--clear-due` to remove its date, time, and reminder.
108
+ `--due` and `--clear-due` cannot be combined. To rename a task inside a page,
109
+ edit its title in that page. Task titles can contain up to 1,000 characters.
110
+
111
+ ## Discover and call more tools
112
+
113
+ Discover tools available to your signed-in connection, including new
114
+ tools that don't have a dedicated CLI command:
115
+
116
+ ```bash
117
+ haunter tools list
118
+ haunter tools describe search_workspace
119
+ haunter tools call search_workspace --input search.json --json
120
+ ```
121
+
122
+ Create `search.json` with the arguments shown by `tools describe`:
123
+
124
+ ```json
125
+ {"query":"meeting notes"}
126
+ ```
127
+
128
+ The CLI checks the current tool list and argument schema each time you call a
129
+ tool. New tools become available as the server adds them, within your approved
130
+ permissions. Run `haunter login` again if you need different access. Tools
131
+ reserved for embedded app interfaces are omitted. Tools requiring background
132
+ MCP tasks are not supported.
133
+
134
+ `--input` accepts a JSON file, or `-` for piped input. It must contain one JSON
135
+ object, up to 1,000,000 bytes. Without `--input`, arguments start as `{}`:
136
+
137
+ ```bash
138
+ printf '%s' '{"query":"meeting notes"}' | haunter tools call search_workspace --input - --json
139
+ haunter tools call list_workspaces --json
140
+ ```
141
+
142
+ For tools with a `workspaceId` argument, the CLI fills it from `--workspace`,
143
+ `HAUNTER_WORKSPACE`, the saved selection, or the only approved workspace.
144
+ A `workspaceId` supplied in JSON takes precedence over saved or environment
145
+ defaults; a conflicting `--workspace` is an error. Tools without that argument
146
+ don't use a default workspace and reject `--workspace`.
147
+
148
+ `tools describe` and `tools call` print JSON, formatted for reading unless
149
+ `--json` is set. Calls preserve the complete MCP result: `content`,
150
+ `structuredContent`, and any metadata. Image data and resource links stay in
151
+ the JSON; the CLI does not render or download them. A tool error also preserves
152
+ the result on stdout, reports an error on stderr, and exits with code 1. Check
153
+ the exit code before treating a result as successful.
154
+
73
155
  ## Use the CLI in scripts
74
156
 
75
157
  Add `--json` to get one JSON value on stdout:
@@ -90,8 +172,9 @@ may have been saved.
90
172
 
91
173
  ## Choose a workspace or server
92
174
 
93
- Pass `--workspace WORKSPACE_ID` (or `-w WORKSPACE_ID`) to a page, capture, or task
94
- command to override the saved workspace.
175
+ Pass `--workspace WORKSPACE_ID` (or `-w WORKSPACE_ID`) to a search, canvas, page,
176
+ capture, task, or workspace-scoped `tools call` command to override the saved
177
+ workspace.
95
178
 
96
179
  | Setting | Flag | Environment variable | Default |
97
180
  | --- | --- | --- | --- |
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { createReadStream } from "node:fs";
4
+ import { createReadStream as createReadStream2 } from "node:fs";
5
5
  import { stripVTControlCharacters } from "node:util";
6
6
  import { z as z2 } from "zod";
7
7
  // package.json
8
- var version = "0.1.0";
8
+ var version = "0.2.1";
9
9
 
10
10
  // src/args.ts
11
11
  import { parseArgs } from "node:util";
@@ -19,7 +19,11 @@ var options = {
19
19
  page: { type: "string" },
20
20
  parent: { type: "string" },
21
21
  file: { type: "string" },
22
+ input: { type: "string" },
22
23
  due: { type: "string" },
24
+ "clear-due": { type: "boolean" },
25
+ kind: { type: "string" },
26
+ cursor: { type: "string" },
23
27
  filter: { type: "string" },
24
28
  scope: { type: "string" },
25
29
  limit: { type: "string" },
@@ -30,18 +34,29 @@ var commands = {
30
34
  login: { args: 0, flags: ["port", "no-browser"] },
31
35
  logout: { args: 0, flags: [] },
32
36
  status: { args: 0, flags: [] },
37
+ search: { args: 1, flags: ["workspace", "kind", "limit", "cursor"] },
38
+ "canvases list": { args: 0, flags: ["workspace"] },
39
+ "tools list": { args: 0, flags: [] },
40
+ "tools describe": { args: 1, flags: [] },
41
+ "tools call": { args: 1, flags: ["input", "workspace"] },
33
42
  "workspaces list": { args: 0, flags: [] },
34
43
  "workspaces use": { args: 1, flags: [] },
35
44
  "pages list": { args: 0, flags: ["workspace"] },
36
45
  "pages search": { args: 1, flags: ["workspace"] },
37
46
  "pages read": { args: 1, flags: ["workspace"] },
47
+ "pages rename": { args: 2, flags: ["workspace"] },
38
48
  capture: { args: 1, flags: ["workspace", "title", "page", "parent", "file"] },
39
49
  "tasks list": {
40
50
  args: 0,
41
51
  flags: ["workspace", "filter", "scope", "limit", "due"]
42
52
  },
43
53
  "tasks add": { args: 1, flags: ["workspace", "due"] },
44
- "tasks done": { args: 1, flags: ["workspace"] }
54
+ "tasks done": { args: 1, flags: ["workspace"] },
55
+ "tasks reopen": { args: 1, flags: ["workspace"] },
56
+ "tasks update": {
57
+ args: 1,
58
+ flags: ["workspace", "title", "due", "clear-due"]
59
+ }
45
60
  };
46
61
  function parseArguments(args) {
47
62
  const parsed = parseArgs({
@@ -53,7 +68,13 @@ function parseArguments(args) {
53
68
  });
54
69
  const { values, positionals, tokens } = parsed;
55
70
  const group = positionals[0];
56
- const grouped = ["pages", "tasks", "workspaces"].includes(group);
71
+ const grouped = [
72
+ "pages",
73
+ "tasks",
74
+ "workspaces",
75
+ "tools",
76
+ "canvases"
77
+ ].includes(group);
57
78
  const command = grouped ? `${group} ${positionals[1] ?? ""}`.trim() : group;
58
79
  const operands = positionals.slice(grouped ? 2 : 1);
59
80
  if (values.help || values.version || !group)
@@ -62,7 +83,7 @@ function parseArguments(args) {
62
83
  if (!spec)
63
84
  throw new Error(`Unknown command: ${command}. Run haunter --help.`);
64
85
  if (operands.length > spec.args || command !== "capture" && operands.length !== spec.args) {
65
- throw new Error(`${command} expects ${spec.args ? "one argument (quote text containing spaces)" : "no arguments"}. Run haunter --help.`);
86
+ throw new Error(`${command} expects ${spec.args === 0 ? "no arguments" : `${spec.args === 1 ? "one argument" : `${spec.args} arguments`} (quote text containing spaces)`}. Run haunter --help.`);
66
87
  }
67
88
  const seen = new Set;
68
89
  for (const token of tokens) {
@@ -87,7 +108,25 @@ function parseArguments(args) {
87
108
  if (values.port)
88
109
  integer(values.port, "port", 1, 65535);
89
110
  if (values.limit)
90
- integer(values.limit, "limit", 1, 200);
111
+ integer(values.limit, "limit", 1, command === "search" ? 50 : 200);
112
+ if (command === "search") {
113
+ if (operands[0].trim().length < 2 || operands[0].trim().length > 200)
114
+ throw new Error("Search queries must be between 2 and 200 characters.");
115
+ if (values.kind && !["all", "page", "task", "canvas"].includes(values.kind))
116
+ throw new Error("--kind must be all, page, task, or canvas.");
117
+ if (values.cursor && values.cursor.length > 4096)
118
+ throw new Error("--cursor must be a cursor returned by search (at most 4,096 characters).");
119
+ }
120
+ if (command === "pages rename" && operands[1].trim().length > 300)
121
+ throw new Error("Page titles must be 300 characters or fewer.");
122
+ if (command === "tasks update") {
123
+ if (!values.title && !values.due && !values["clear-due"])
124
+ throw new Error("Provide --title, --due, or --clear-due to update a task.");
125
+ if (values.due && values["clear-due"])
126
+ throw new Error("Use --due or --clear-due, not both.");
127
+ if (values.title && values.title.trim().length > 1000)
128
+ throw new Error("Task titles must be 1,000 characters or fewer.");
129
+ }
91
130
  if (values.filter && !["open", "completed", "all"].includes(values.filter))
92
131
  throw new Error("--filter must be open, completed, or all.");
93
132
  if (values.scope && !["mine", "everyone"].includes(values.scope))
@@ -97,7 +136,7 @@ function parseArguments(args) {
97
136
  if (!["today", "overdue", "upcoming"].includes(values.due))
98
137
  throw new Error("--due must be today, overdue, or upcoming for tasks list.");
99
138
  } else if (!/^\d{4}-\d{2}-\d{2}$/.test(values.due) || Number.isNaN(Date.parse(values.due)) || new Date(values.due).toISOString().slice(0, 10) !== values.due) {
100
- throw new Error("--due must be a valid date in YYYY-MM-DD format for tasks add.");
139
+ throw new Error("--due must be a valid date in YYYY-MM-DD format for tasks add or update.");
101
140
  }
102
141
  }
103
142
  return { command, operands, values, help: false };
@@ -117,9 +156,15 @@ Usage: haunter <command> [options]
117
156
  status Show local sign-in and workspace settings
118
157
  workspaces list List workspaces you approved
119
158
  workspaces use <id> Save the default workspace for this server
159
+ search <query> Search pages, tasks, and canvases
160
+ --kind <all|page|task|canvas> Filter the result type (default: all)
161
+ --limit <1-50> Results per request (default: 20)
162
+ --cursor <cursor> Continue the same search from its next cursor
163
+ canvases list List standalone canvases
120
164
  pages list List pages
121
165
  pages search <query> Search pages (quote phrases)
122
166
  pages read <id> Print a page as Markdown
167
+ pages rename <id> <title> Rename a page (quote titles containing spaces)
123
168
  capture [text] Create a page from text, a file, or stdin
124
169
  --title <title> New page title (default: first nonempty line)
125
170
  --file <path> Read Markdown from a file; use - for stdin
@@ -132,6 +177,15 @@ Usage: haunter <command> [options]
132
177
  --limit <1-200> Maximum tasks (default: 50)
133
178
  tasks add <title> Create a task; optional --due YYYY-MM-DD
134
179
  tasks done <id> Complete a task
180
+ tasks reopen <id> Reopen a completed task
181
+ tasks update <id> Update a task's title or due date
182
+ --title <title> New title for a standalone task
183
+ --due <YYYY-MM-DD> Set the due date
184
+ --clear-due Clear the date, time, and reminder
185
+ tools list Discover tools available to your connection
186
+ tools describe <name> Show a tool's description and JSON schemas
187
+ tools call <name> Call a discovered tool with JSON arguments
188
+ --input <path|-> Read a JSON object from a file or stdin
135
189
 
136
190
  Options:
137
191
  -w, --workspace <id> Override the saved workspace for a command
@@ -177,6 +231,7 @@ class AuthProvider {
177
231
  }
178
232
  get clientMetadata() {
179
233
  return {
234
+ application_type: "native",
180
235
  client_name: "Haunter CLI",
181
236
  client_uri: "https://www.haunter.app",
182
237
  redirect_uris: [this.redirectUrl],
@@ -366,6 +421,10 @@ ${url.href}`);
366
421
  // src/client.ts
367
422
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
368
423
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
424
+ import {
425
+ CallToolResultSchema,
426
+ ListToolsResultSchema
427
+ } from "@modelcontextprotocol/sdk/types.js";
369
428
  async function connect(store, profile, signal) {
370
429
  if (!profile.tokens)
371
430
  throw new Error(LOGIN_REQUIRED);
@@ -387,6 +446,32 @@ async function connect(store, profile, signal) {
387
446
  throw error;
388
447
  }
389
448
  return {
449
+ async listTools() {
450
+ const tools = [];
451
+ const names = new Set;
452
+ const cursors = new Set;
453
+ let cursor;
454
+ for (let page = 0;page < 100; page++) {
455
+ const result = await client.request({
456
+ method: "tools/list",
457
+ params: cursor === undefined ? {} : { cursor }
458
+ }, ListToolsResultSchema, { timeout: 30000, signal });
459
+ for (const tool of result.tools) {
460
+ if (names.has(tool.name))
461
+ throw new Error(`Haunter listed duplicate tool ${tool.name}.`);
462
+ names.add(tool.name);
463
+ tools.push(tool);
464
+ }
465
+ cursor = result.nextCursor;
466
+ if (cursor === undefined)
467
+ return tools;
468
+ if (cursors.has(cursor))
469
+ throw new Error("Haunter repeated a tool-list cursor.");
470
+ cursors.add(cursor);
471
+ }
472
+ throw new Error("Haunter's tool list exceeded 100 pages.");
473
+ },
474
+ callTool: (name, args) => client.request({ method: "tools/call", params: { name, arguments: args } }, CallToolResultSchema, { timeout: 30000, signal }),
390
475
  async call(name, args) {
391
476
  const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 30000, signal });
392
477
  const text = Array.isArray(result.content) ? result.content.filter((item) => item.type === "text").map((item) => item.text).join(`
@@ -405,6 +490,50 @@ async function connect(store, profile, signal) {
405
490
  };
406
491
  }
407
492
 
493
+ // src/tools.ts
494
+ import { createReadStream } from "node:fs";
495
+ import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
496
+ async function readToolInput(file, io) {
497
+ if (!file)
498
+ return {};
499
+ if (file === "-" && io.isTTY)
500
+ throw new Error("Pipe a JSON object into tools call --input -.");
501
+ const stream = file === "-" ? io.stdin : createReadStream(file);
502
+ const chunks = [];
503
+ let bytes = 0;
504
+ for await (const chunk of stream) {
505
+ io.signal?.throwIfAborted();
506
+ const buffer = Buffer.from(chunk);
507
+ bytes += buffer.length;
508
+ if (bytes > 1e6)
509
+ throw new Error("Tool input exceeds 1,000,000 bytes.");
510
+ chunks.push(buffer);
511
+ }
512
+ let input;
513
+ try {
514
+ input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
515
+ } catch {
516
+ throw new Error("Tool input must be valid JSON.");
517
+ }
518
+ if (!input || typeof input !== "object" || Array.isArray(input))
519
+ throw new Error("Tool input must be a JSON object.");
520
+ return input;
521
+ }
522
+ function isCliTool(tool) {
523
+ const ui = tool._meta?.ui;
524
+ if (!ui || typeof ui !== "object")
525
+ return true;
526
+ const visibility = ui.visibility;
527
+ return !Array.isArray(visibility) || visibility.includes("model");
528
+ }
529
+ function toolValidator(schema, name) {
530
+ try {
531
+ return new CfWorkerJsonSchemaValidator().getValidator(schema);
532
+ } catch {
533
+ throw new Error(`Cannot validate the schema for ${name}. Update the CLI and try again.`);
534
+ }
535
+ }
536
+
408
537
  // src/config.ts
409
538
  import { createHash, randomUUID } from "node:crypto";
410
539
  import { chmod, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
@@ -520,6 +649,24 @@ var taskRow = z2.object({
520
649
  }).passthrough();
521
650
  var tasksResult = z2.object({ tasks: z2.array(taskRow), hasMore: z2.boolean() });
522
651
  var savedPage = z2.object({ pageId: z2.string(), title: z2.string() });
652
+ var canvasesResult = z2.object({
653
+ canvases: z2.array(z2.object({ canvasId: z2.string(), title: z2.string() }).passthrough())
654
+ }).passthrough();
655
+ var searchResult = z2.object({
656
+ items: z2.array(z2.object({
657
+ kind: z2.enum(["page", "task", "canvas"]),
658
+ id: z2.string(),
659
+ title: z2.string(),
660
+ snippet: z2.string()
661
+ }).passthrough()),
662
+ nextCursor: z2.string().nullable()
663
+ }).passthrough();
664
+ var taskActions = {
665
+ "tasks add": { tool: "create_task", label: "Created" },
666
+ "tasks done": { tool: "complete_task", label: "Completed" },
667
+ "tasks reopen": { tool: "reopen_task", label: "Reopened" },
668
+ "tasks update": { tool: "update_task", label: "Updated" }
669
+ };
523
670
  async function runCli(args, io, dependencies = { connect, login }) {
524
671
  let secrets = [];
525
672
  let release;
@@ -539,6 +686,8 @@ async function runCli(args, io, dependencies = { connect, login }) {
539
686
  write({ help: HELP }, HELP.trimEnd());
540
687
  return 0;
541
688
  }
689
+ if (["pages rename", "tasks update", "tasks reopen"].includes(command))
690
+ uuid(operands[0]);
542
691
  const server = serverUrl(values.server ?? io.env.HAUNTER_SERVER ?? DEFAULT_SERVER);
543
692
  const store = new ProfileStore(configDirectory(io.env), server);
544
693
  release = await store.lock();
@@ -578,7 +727,46 @@ Workspace: ${status.workspaceId ?? "not selected"}`);
578
727
  return 0;
579
728
  }
580
729
  const markdown = command === "capture" ? await captureText(operands[0], values.file, io) : undefined;
730
+ const toolInput = command === "tools call" ? await readToolInput(values.input, io) : {};
581
731
  client = await dependencies.connect(store, profile, io.signal);
732
+ if (command.startsWith("tools ")) {
733
+ const tools = (await client.listTools()).filter(isCliTool);
734
+ if (command === "tools list") {
735
+ write({ tools }, tools.map((tool) => `${tool.name} ${oneLine(tool.description ?? tool.title ?? "")}`).join(`
736
+ `) || "No tools available. Run haunter login to review access.");
737
+ return 0;
738
+ }
739
+ const tool = tools.find((item) => item.name === operands[0]);
740
+ if (!tool)
741
+ throw new Error(`Tool ${operands[0]} is not available to this connection. Run haunter tools list, or haunter login to review access.`);
742
+ if (command === "tools describe") {
743
+ write(tool, JSON.stringify(tool, null, 2));
744
+ return 0;
745
+ }
746
+ if (tool.execution?.taskSupport === "required")
747
+ throw new Error(`${tool.name} requires MCP task support, which this CLI does not support.`);
748
+ if (Object.hasOwn(tool.inputSchema.properties ?? {}, "workspaceId")) {
749
+ if (Object.hasOwn(toolInput, "workspaceId")) {
750
+ if (values.workspace && values.workspace !== toolInput.workspaceId)
751
+ throw new Error("--workspace conflicts with workspaceId in the JSON input.");
752
+ } else {
753
+ toolInput.workspaceId = await resolveWorkspace(client, values.workspace ?? io.env.HAUNTER_WORKSPACE ?? profile.workspaceId);
754
+ }
755
+ } else if (values.workspace) {
756
+ throw new Error(`${tool.name} does not accept --workspace.`);
757
+ }
758
+ const validation = toolValidator(tool.inputSchema, tool.name)(toolInput);
759
+ if (!validation.valid)
760
+ throw new Error(`Invalid arguments for ${tool.name}: ${validation.errorMessage}`);
761
+ const validateOutput = tool.outputSchema ? toolValidator(tool.outputSchema, tool.name) : undefined;
762
+ const result = await client.callTool(tool.name, toolInput);
763
+ write(result, JSON.stringify(result, null, 2));
764
+ if (result.isError)
765
+ throw new Error(`${tool.name} failed. See the tool result on stdout.`);
766
+ if (validateOutput && !validateOutput(result.structuredContent).valid)
767
+ throw new Error(`${tool.name} returned a result that does not match its output schema. The call was sent; check Haunter before repeating it.`);
768
+ return 0;
769
+ }
582
770
  if (command === "workspaces list" || command === "workspaces use") {
583
771
  const result = workspaceResult.parse(await client.call("list_workspaces", {}));
584
772
  if (command === "workspaces list")
@@ -594,15 +782,37 @@ Workspace: ${status.workspaceId ?? "not selected"}`);
594
782
  }
595
783
  return 0;
596
784
  }
597
- let workspaceId = values.workspace ?? io.env.HAUNTER_WORKSPACE ?? profile.workspaceId;
598
- if (!workspaceId) {
599
- const result = workspaceResult.parse(await client.call("list_workspaces", {}));
600
- if (result.workspaces.length !== 1)
601
- throw new Error("Select a workspace with haunter workspaces use <id>, or pass --workspace <id>.");
602
- workspaceId = result.workspaces[0].id;
603
- }
785
+ const workspaceId = await resolveWorkspace(client, values.workspace ?? io.env.HAUNTER_WORKSPACE ?? profile.workspaceId);
604
786
  const workspace = { workspaceId };
605
- if (command === "pages list" || command === "pages search") {
787
+ if (command === "search") {
788
+ const result = searchResult.parse(await client.call("search_workspace", {
789
+ ...workspace,
790
+ query: operands[0].trim(),
791
+ kind: values.kind ?? "all",
792
+ limit: values.limit ? Number(values.limit) : 20,
793
+ ...values.cursor ? { cursor: values.cursor } : {}
794
+ }));
795
+ write(result, result.items.map((item) => `${item.kind} ${item.id} ${oneLine(item.title || "Untitled")}${item.snippet ? `
796
+ ${oneLine(item.snippet)}` : ""}`).join(`
797
+ `) || "No results found.");
798
+ if (result.nextCursor && !json)
799
+ io.stderr(`More results available. Repeat the same query and --kind with --cursor.
800
+ Next cursor: ${oneLine(result.nextCursor)}
801
+ `);
802
+ } else if (command === "canvases list") {
803
+ const result = canvasesResult.parse(await client.call("list_canvases", workspace));
804
+ write(result, result.canvases.map((canvas) => `${canvas.canvasId} ${oneLine(canvas.title || "Untitled")}`).join(`
805
+ `) || "No standalone canvases found.");
806
+ } else if (command === "pages rename") {
807
+ const result = await client.call("update_page", {
808
+ ...workspace,
809
+ pageId: operands[0],
810
+ title: operands[1].trim()
811
+ });
812
+ const page = savedPage.parse(result);
813
+ write(result, `Renamed to ${oneLine(page.title)}
814
+ ${page.pageId}`);
815
+ } else if (command === "pages list" || command === "pages search") {
606
816
  const result = pagesResult.parse(await client.call(command === "pages list" ? "list_pages" : "search_pages", {
607
817
  ...workspace,
608
818
  ...command === "pages search" ? { query: operands[0] } : {}
@@ -643,15 +853,23 @@ ${page.pageId}`);
643
853
  io.stderr(`More tasks are available. Narrow the filters or increase --limit (maximum 200).
644
854
  `);
645
855
  } else {
646
- const result = await client.call(command === "tasks add" ? "create_task" : "complete_task", {
856
+ const action = taskActions[command];
857
+ if (!action)
858
+ throw new Error(`Unsupported command: ${command}. Run haunter --help.`);
859
+ const result = await client.call(action.tool, {
647
860
  ...workspace,
648
861
  ...command === "tasks add" ? {
649
862
  title: operands[0],
650
863
  ...values.due ? { dueDate: values.due } : {}
651
- } : { taskId: uuid(operands[0]) }
864
+ } : { taskId: uuid(operands[0]) },
865
+ ...command === "tasks update" ? {
866
+ ...values.title !== undefined ? { title: values.title.trim() } : {},
867
+ ...values.due !== undefined ? { dueDate: values.due } : {},
868
+ ...values["clear-due"] ? { dueDate: null } : {}
869
+ } : {}
652
870
  });
653
871
  const task = taskRow.parse(result);
654
- write(result, `${command === "tasks add" ? "Created" : "Completed"} ${oneLine(task.title)}
872
+ write(result, `${action.label} ${oneLine(task.title)}
655
873
  ${task.taskId}`);
656
874
  }
657
875
  return 0;
@@ -673,6 +891,14 @@ ${task.taskId}`);
673
891
  });
674
892
  }
675
893
  }
894
+ async function resolveWorkspace(client, selected) {
895
+ if (selected)
896
+ return selected;
897
+ const result = workspaceResult.parse(await client.call("list_workspaces", {}));
898
+ if (result.workspaces.length !== 1)
899
+ throw new Error("Select a workspace with haunter workspaces use <id>, or pass --workspace <id>.");
900
+ return result.workspaces[0].id;
901
+ }
676
902
  function uuid(value) {
677
903
  if (!z2.uuid().safeParse(value).success)
678
904
  throw new Error("Expected a page or task ID in UUID format.");
@@ -686,7 +912,7 @@ async function captureText(text, file, io) {
686
912
  }
687
913
  if (!file && io.isTTY)
688
914
  throw new Error("Provide capture text, --file <path>, or pipe Markdown into haunter capture.");
689
- const input = file && file !== "-" ? createReadStream(file) : io.stdin;
915
+ const input = file && file !== "-" ? createReadStream2(file) : io.stdin;
690
916
  const chunks = [];
691
917
  let bytes = 0;
692
918
  for await (const chunk of input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haunter",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pages, notes, and tasks in Haunter from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,7 @@
46
46
  "prepack": "bun run check"
47
47
  },
48
48
  "dependencies": {
49
+ "@cfworker/json-schema": "4.1.1",
49
50
  "@modelcontextprotocol/sdk": "1.32.1",
50
51
  "zod": "4.6.7"
51
52
  },