haunter 0.1.0 → 0.2.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/README.md +85 -2
- package/dist/index.js +244 -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.
|
|
8
|
+
var version = "0.2.0";
|
|
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
|
|
@@ -366,6 +420,10 @@ ${url.href}`);
|
|
|
366
420
|
// src/client.ts
|
|
367
421
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
368
422
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
423
|
+
import {
|
|
424
|
+
CallToolResultSchema,
|
|
425
|
+
ListToolsResultSchema
|
|
426
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
369
427
|
async function connect(store, profile, signal) {
|
|
370
428
|
if (!profile.tokens)
|
|
371
429
|
throw new Error(LOGIN_REQUIRED);
|
|
@@ -387,6 +445,32 @@ async function connect(store, profile, signal) {
|
|
|
387
445
|
throw error;
|
|
388
446
|
}
|
|
389
447
|
return {
|
|
448
|
+
async listTools() {
|
|
449
|
+
const tools = [];
|
|
450
|
+
const names = new Set;
|
|
451
|
+
const cursors = new Set;
|
|
452
|
+
let cursor;
|
|
453
|
+
for (let page = 0;page < 100; page++) {
|
|
454
|
+
const result = await client.request({
|
|
455
|
+
method: "tools/list",
|
|
456
|
+
params: cursor === undefined ? {} : { cursor }
|
|
457
|
+
}, ListToolsResultSchema, { timeout: 30000, signal });
|
|
458
|
+
for (const tool of result.tools) {
|
|
459
|
+
if (names.has(tool.name))
|
|
460
|
+
throw new Error(`Haunter listed duplicate tool ${tool.name}.`);
|
|
461
|
+
names.add(tool.name);
|
|
462
|
+
tools.push(tool);
|
|
463
|
+
}
|
|
464
|
+
cursor = result.nextCursor;
|
|
465
|
+
if (cursor === undefined)
|
|
466
|
+
return tools;
|
|
467
|
+
if (cursors.has(cursor))
|
|
468
|
+
throw new Error("Haunter repeated a tool-list cursor.");
|
|
469
|
+
cursors.add(cursor);
|
|
470
|
+
}
|
|
471
|
+
throw new Error("Haunter's tool list exceeded 100 pages.");
|
|
472
|
+
},
|
|
473
|
+
callTool: (name, args) => client.request({ method: "tools/call", params: { name, arguments: args } }, CallToolResultSchema, { timeout: 30000, signal }),
|
|
390
474
|
async call(name, args) {
|
|
391
475
|
const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 30000, signal });
|
|
392
476
|
const text = Array.isArray(result.content) ? result.content.filter((item) => item.type === "text").map((item) => item.text).join(`
|
|
@@ -405,6 +489,50 @@ async function connect(store, profile, signal) {
|
|
|
405
489
|
};
|
|
406
490
|
}
|
|
407
491
|
|
|
492
|
+
// src/tools.ts
|
|
493
|
+
import { createReadStream } from "node:fs";
|
|
494
|
+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
|
495
|
+
async function readToolInput(file, io) {
|
|
496
|
+
if (!file)
|
|
497
|
+
return {};
|
|
498
|
+
if (file === "-" && io.isTTY)
|
|
499
|
+
throw new Error("Pipe a JSON object into tools call --input -.");
|
|
500
|
+
const stream = file === "-" ? io.stdin : createReadStream(file);
|
|
501
|
+
const chunks = [];
|
|
502
|
+
let bytes = 0;
|
|
503
|
+
for await (const chunk of stream) {
|
|
504
|
+
io.signal?.throwIfAborted();
|
|
505
|
+
const buffer = Buffer.from(chunk);
|
|
506
|
+
bytes += buffer.length;
|
|
507
|
+
if (bytes > 1e6)
|
|
508
|
+
throw new Error("Tool input exceeds 1,000,000 bytes.");
|
|
509
|
+
chunks.push(buffer);
|
|
510
|
+
}
|
|
511
|
+
let input;
|
|
512
|
+
try {
|
|
513
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
514
|
+
} catch {
|
|
515
|
+
throw new Error("Tool input must be valid JSON.");
|
|
516
|
+
}
|
|
517
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
518
|
+
throw new Error("Tool input must be a JSON object.");
|
|
519
|
+
return input;
|
|
520
|
+
}
|
|
521
|
+
function isCliTool(tool) {
|
|
522
|
+
const ui = tool._meta?.ui;
|
|
523
|
+
if (!ui || typeof ui !== "object")
|
|
524
|
+
return true;
|
|
525
|
+
const visibility = ui.visibility;
|
|
526
|
+
return !Array.isArray(visibility) || visibility.includes("model");
|
|
527
|
+
}
|
|
528
|
+
function toolValidator(schema, name) {
|
|
529
|
+
try {
|
|
530
|
+
return new CfWorkerJsonSchemaValidator().getValidator(schema);
|
|
531
|
+
} catch {
|
|
532
|
+
throw new Error(`Cannot validate the schema for ${name}. Update the CLI and try again.`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
408
536
|
// src/config.ts
|
|
409
537
|
import { createHash, randomUUID } from "node:crypto";
|
|
410
538
|
import { chmod, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
@@ -520,6 +648,24 @@ var taskRow = z2.object({
|
|
|
520
648
|
}).passthrough();
|
|
521
649
|
var tasksResult = z2.object({ tasks: z2.array(taskRow), hasMore: z2.boolean() });
|
|
522
650
|
var savedPage = z2.object({ pageId: z2.string(), title: z2.string() });
|
|
651
|
+
var canvasesResult = z2.object({
|
|
652
|
+
canvases: z2.array(z2.object({ canvasId: z2.string(), title: z2.string() }).passthrough())
|
|
653
|
+
}).passthrough();
|
|
654
|
+
var searchResult = z2.object({
|
|
655
|
+
items: z2.array(z2.object({
|
|
656
|
+
kind: z2.enum(["page", "task", "canvas"]),
|
|
657
|
+
id: z2.string(),
|
|
658
|
+
title: z2.string(),
|
|
659
|
+
snippet: z2.string()
|
|
660
|
+
}).passthrough()),
|
|
661
|
+
nextCursor: z2.string().nullable()
|
|
662
|
+
}).passthrough();
|
|
663
|
+
var taskActions = {
|
|
664
|
+
"tasks add": { tool: "create_task", label: "Created" },
|
|
665
|
+
"tasks done": { tool: "complete_task", label: "Completed" },
|
|
666
|
+
"tasks reopen": { tool: "reopen_task", label: "Reopened" },
|
|
667
|
+
"tasks update": { tool: "update_task", label: "Updated" }
|
|
668
|
+
};
|
|
523
669
|
async function runCli(args, io, dependencies = { connect, login }) {
|
|
524
670
|
let secrets = [];
|
|
525
671
|
let release;
|
|
@@ -539,6 +685,8 @@ async function runCli(args, io, dependencies = { connect, login }) {
|
|
|
539
685
|
write({ help: HELP }, HELP.trimEnd());
|
|
540
686
|
return 0;
|
|
541
687
|
}
|
|
688
|
+
if (["pages rename", "tasks update", "tasks reopen"].includes(command))
|
|
689
|
+
uuid(operands[0]);
|
|
542
690
|
const server = serverUrl(values.server ?? io.env.HAUNTER_SERVER ?? DEFAULT_SERVER);
|
|
543
691
|
const store = new ProfileStore(configDirectory(io.env), server);
|
|
544
692
|
release = await store.lock();
|
|
@@ -578,7 +726,46 @@ Workspace: ${status.workspaceId ?? "not selected"}`);
|
|
|
578
726
|
return 0;
|
|
579
727
|
}
|
|
580
728
|
const markdown = command === "capture" ? await captureText(operands[0], values.file, io) : undefined;
|
|
729
|
+
const toolInput = command === "tools call" ? await readToolInput(values.input, io) : {};
|
|
581
730
|
client = await dependencies.connect(store, profile, io.signal);
|
|
731
|
+
if (command.startsWith("tools ")) {
|
|
732
|
+
const tools = (await client.listTools()).filter(isCliTool);
|
|
733
|
+
if (command === "tools list") {
|
|
734
|
+
write({ tools }, tools.map((tool) => `${tool.name} ${oneLine(tool.description ?? tool.title ?? "")}`).join(`
|
|
735
|
+
`) || "No tools available. Run haunter login to review access.");
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
const tool = tools.find((item) => item.name === operands[0]);
|
|
739
|
+
if (!tool)
|
|
740
|
+
throw new Error(`Tool ${operands[0]} is not available to this connection. Run haunter tools list, or haunter login to review access.`);
|
|
741
|
+
if (command === "tools describe") {
|
|
742
|
+
write(tool, JSON.stringify(tool, null, 2));
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
if (tool.execution?.taskSupport === "required")
|
|
746
|
+
throw new Error(`${tool.name} requires MCP task support, which this CLI does not support.`);
|
|
747
|
+
if (Object.hasOwn(tool.inputSchema.properties ?? {}, "workspaceId")) {
|
|
748
|
+
if (Object.hasOwn(toolInput, "workspaceId")) {
|
|
749
|
+
if (values.workspace && values.workspace !== toolInput.workspaceId)
|
|
750
|
+
throw new Error("--workspace conflicts with workspaceId in the JSON input.");
|
|
751
|
+
} else {
|
|
752
|
+
toolInput.workspaceId = await resolveWorkspace(client, values.workspace ?? io.env.HAUNTER_WORKSPACE ?? profile.workspaceId);
|
|
753
|
+
}
|
|
754
|
+
} else if (values.workspace) {
|
|
755
|
+
throw new Error(`${tool.name} does not accept --workspace.`);
|
|
756
|
+
}
|
|
757
|
+
const validation = toolValidator(tool.inputSchema, tool.name)(toolInput);
|
|
758
|
+
if (!validation.valid)
|
|
759
|
+
throw new Error(`Invalid arguments for ${tool.name}: ${validation.errorMessage}`);
|
|
760
|
+
const validateOutput = tool.outputSchema ? toolValidator(tool.outputSchema, tool.name) : undefined;
|
|
761
|
+
const result = await client.callTool(tool.name, toolInput);
|
|
762
|
+
write(result, JSON.stringify(result, null, 2));
|
|
763
|
+
if (result.isError)
|
|
764
|
+
throw new Error(`${tool.name} failed. See the tool result on stdout.`);
|
|
765
|
+
if (validateOutput && !validateOutput(result.structuredContent).valid)
|
|
766
|
+
throw new Error(`${tool.name} returned a result that does not match its output schema. The call was sent; check Haunter before repeating it.`);
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
582
769
|
if (command === "workspaces list" || command === "workspaces use") {
|
|
583
770
|
const result = workspaceResult.parse(await client.call("list_workspaces", {}));
|
|
584
771
|
if (command === "workspaces list")
|
|
@@ -594,15 +781,37 @@ Workspace: ${status.workspaceId ?? "not selected"}`);
|
|
|
594
781
|
}
|
|
595
782
|
return 0;
|
|
596
783
|
}
|
|
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
|
-
}
|
|
784
|
+
const workspaceId = await resolveWorkspace(client, values.workspace ?? io.env.HAUNTER_WORKSPACE ?? profile.workspaceId);
|
|
604
785
|
const workspace = { workspaceId };
|
|
605
|
-
if (command === "
|
|
786
|
+
if (command === "search") {
|
|
787
|
+
const result = searchResult.parse(await client.call("search_workspace", {
|
|
788
|
+
...workspace,
|
|
789
|
+
query: operands[0].trim(),
|
|
790
|
+
kind: values.kind ?? "all",
|
|
791
|
+
limit: values.limit ? Number(values.limit) : 20,
|
|
792
|
+
...values.cursor ? { cursor: values.cursor } : {}
|
|
793
|
+
}));
|
|
794
|
+
write(result, result.items.map((item) => `${item.kind} ${item.id} ${oneLine(item.title || "Untitled")}${item.snippet ? `
|
|
795
|
+
${oneLine(item.snippet)}` : ""}`).join(`
|
|
796
|
+
`) || "No results found.");
|
|
797
|
+
if (result.nextCursor && !json)
|
|
798
|
+
io.stderr(`More results available. Repeat the same query and --kind with --cursor.
|
|
799
|
+
Next cursor: ${oneLine(result.nextCursor)}
|
|
800
|
+
`);
|
|
801
|
+
} else if (command === "canvases list") {
|
|
802
|
+
const result = canvasesResult.parse(await client.call("list_canvases", workspace));
|
|
803
|
+
write(result, result.canvases.map((canvas) => `${canvas.canvasId} ${oneLine(canvas.title || "Untitled")}`).join(`
|
|
804
|
+
`) || "No standalone canvases found.");
|
|
805
|
+
} else if (command === "pages rename") {
|
|
806
|
+
const result = await client.call("update_page", {
|
|
807
|
+
...workspace,
|
|
808
|
+
pageId: operands[0],
|
|
809
|
+
title: operands[1].trim()
|
|
810
|
+
});
|
|
811
|
+
const page = savedPage.parse(result);
|
|
812
|
+
write(result, `Renamed to ${oneLine(page.title)}
|
|
813
|
+
${page.pageId}`);
|
|
814
|
+
} else if (command === "pages list" || command === "pages search") {
|
|
606
815
|
const result = pagesResult.parse(await client.call(command === "pages list" ? "list_pages" : "search_pages", {
|
|
607
816
|
...workspace,
|
|
608
817
|
...command === "pages search" ? { query: operands[0] } : {}
|
|
@@ -643,15 +852,23 @@ ${page.pageId}`);
|
|
|
643
852
|
io.stderr(`More tasks are available. Narrow the filters or increase --limit (maximum 200).
|
|
644
853
|
`);
|
|
645
854
|
} else {
|
|
646
|
-
const
|
|
855
|
+
const action = taskActions[command];
|
|
856
|
+
if (!action)
|
|
857
|
+
throw new Error(`Unsupported command: ${command}. Run haunter --help.`);
|
|
858
|
+
const result = await client.call(action.tool, {
|
|
647
859
|
...workspace,
|
|
648
860
|
...command === "tasks add" ? {
|
|
649
861
|
title: operands[0],
|
|
650
862
|
...values.due ? { dueDate: values.due } : {}
|
|
651
|
-
} : { taskId: uuid(operands[0]) }
|
|
863
|
+
} : { taskId: uuid(operands[0]) },
|
|
864
|
+
...command === "tasks update" ? {
|
|
865
|
+
...values.title !== undefined ? { title: values.title.trim() } : {},
|
|
866
|
+
...values.due !== undefined ? { dueDate: values.due } : {},
|
|
867
|
+
...values["clear-due"] ? { dueDate: null } : {}
|
|
868
|
+
} : {}
|
|
652
869
|
});
|
|
653
870
|
const task = taskRow.parse(result);
|
|
654
|
-
write(result, `${
|
|
871
|
+
write(result, `${action.label} ${oneLine(task.title)}
|
|
655
872
|
${task.taskId}`);
|
|
656
873
|
}
|
|
657
874
|
return 0;
|
|
@@ -673,6 +890,14 @@ ${task.taskId}`);
|
|
|
673
890
|
});
|
|
674
891
|
}
|
|
675
892
|
}
|
|
893
|
+
async function resolveWorkspace(client, selected) {
|
|
894
|
+
if (selected)
|
|
895
|
+
return selected;
|
|
896
|
+
const result = workspaceResult.parse(await client.call("list_workspaces", {}));
|
|
897
|
+
if (result.workspaces.length !== 1)
|
|
898
|
+
throw new Error("Select a workspace with haunter workspaces use <id>, or pass --workspace <id>.");
|
|
899
|
+
return result.workspaces[0].id;
|
|
900
|
+
}
|
|
676
901
|
function uuid(value) {
|
|
677
902
|
if (!z2.uuid().safeParse(value).success)
|
|
678
903
|
throw new Error("Expected a page or task ID in UUID format.");
|
|
@@ -686,7 +911,7 @@ async function captureText(text, file, io) {
|
|
|
686
911
|
}
|
|
687
912
|
if (!file && io.isTTY)
|
|
688
913
|
throw new Error("Provide capture text, --file <path>, or pipe Markdown into haunter capture.");
|
|
689
|
-
const input = file && file !== "-" ?
|
|
914
|
+
const input = file && file !== "-" ? createReadStream2(file) : io.stdin;
|
|
690
915
|
const chunks = [];
|
|
691
916
|
let bytes = 0;
|
|
692
917
|
for await (const chunk of input) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haunter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
},
|