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.
- package/README.md +85 -2
- package/dist/index.js +245 -19
- 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
|
|
94
|
-
command to override the saved
|
|
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
|
|
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 = [
|
|
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)
|
|
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
|
-
|
|
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 === "
|
|
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
|
|
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, `${
|
|
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 !== "-" ?
|
|
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
|
|
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
|
},
|