@theplato/tiro-cli 0.4.0 → 0.5.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/AGENTS.md +12 -23
- package/README.md +26 -3
- package/dist/bin/tiro.js +598 -24
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -89,26 +89,15 @@ Two ways to authenticate:
|
|
|
89
89
|
The CLI never prints tokens to stdout. `tiro auth status` shows only the
|
|
90
90
|
first 4 characters of the access token.
|
|
91
91
|
|
|
92
|
-
## Security
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| **H4** | `lib/config.ts` `getOauthClientId` | DCR `client_id` cache is bound to the hostname it was registered against; a swapped hostname misses the cache. |
|
|
105
|
-
| **M1** | `lib/output/file.ts` `assertSafeOutputPath` | `--output` rejects `..` segments unless `--force`; an agent forwarding untrusted input cannot escape its scope. |
|
|
106
|
-
| **M2** | `lib/auth/token.ts` `decodeJwtPayload` | Documented as display-only — claims must never gate auth, scope, identity, or expiry decisions. |
|
|
107
|
-
|
|
108
|
-
Pre-deploy checklist:
|
|
109
|
-
|
|
110
|
-
1. `npm run typecheck`
|
|
111
|
-
2. `npm run test:security` (regression gate)
|
|
112
|
-
3. `npm test` (full suite)
|
|
113
|
-
4. `npm run build`
|
|
114
|
-
5. Smoke: `node dist/bin/tiro.js auth status`
|
|
92
|
+
## Security posture (summary)
|
|
93
|
+
|
|
94
|
+
- OAuth uses Authorization Code + PKCE with a CSRF-checked loopback
|
|
95
|
+
redirect. Tokens are held in the OS keychain or `TIRO_TOKEN` only —
|
|
96
|
+
never written to stdout, stderr, or disk in any other form.
|
|
97
|
+
- `TIRO_HOSTNAME` / `--hostname` are validated against an `https://` (or
|
|
98
|
+
loopback) allowlist before any OAuth or API request runs.
|
|
99
|
+
- `--output` paths that escape the working tree require `--force`.
|
|
100
|
+
- A dedicated regression suite (`npm run test:security`) pins these
|
|
101
|
+
surfaces and runs in `prepublishOnly` — a broken defense blocks publish.
|
|
102
|
+
|
|
103
|
+
Always run on the latest released version; older versions are deprecated.
|
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ The CLI is **not a replacement for MCP**. It's the same data through a different
|
|
|
46
46
|
- 🔐 **OAuth Authorization Code + PKCE** — no copy-paste tokens, no secrets in shell history. Tokens live in OS Keychain.
|
|
47
47
|
- 📁 **`--output` writes to disk** — stdout becomes a single line of metadata. Agents stay context-light.
|
|
48
48
|
- 🪞 **Same JSON shape as MCP** — `tiro notes transcript --format json` mirrors `get_note_transcript` exactly. Switch surfaces without changing parsers.
|
|
49
|
+
- 🕸️ **Workspace wiki** — `tiro wiki search / page / mentions / graph` read the auto-extracted knowledge graph; `--workspace` targets a specific workspace.
|
|
49
50
|
- 🧵 **NDJSON streams by default** — pipe to `jq`, `head`, `xargs`. No buffer-in-memory surprises.
|
|
50
51
|
- 🤖 **Agent-aware** — TTY detection auto-selects pretty vs JSON. `error.suggestion` field for auto-recovery.
|
|
51
52
|
- 🚫 **No voice in v1** — intentionally matches MCP feature parity (audio uploads belong elsewhere).
|
|
@@ -108,7 +109,7 @@ tiro notes list
|
|
|
108
109
|
```bash
|
|
109
110
|
tiro notes list # pretty table in TTY
|
|
110
111
|
tiro notes list --json # NDJSON for pipes
|
|
111
|
-
tiro notes list --keyword "OKR" --since 30d #
|
|
112
|
+
tiro notes list --keyword "OKR" --since 30d # Reorder by keyword relevance
|
|
112
113
|
tiro notes list --folder <id> --limit 100 --cursor <token>
|
|
113
114
|
```
|
|
114
115
|
|
|
@@ -150,6 +151,28 @@ header instead of being attached to every speaker line. Use
|
|
|
150
151
|
|
|
151
152
|
`--format json` returns the exact shape MCP's `get_note_transcript` emits (`{noteGuid, title, participants, createdAt, recordingDurationSeconds, paragraphs[]}` with each paragraph carrying `segments[]` of `{content, speaker:{label,name}|null}`).
|
|
152
153
|
|
|
154
|
+
### Explore the workspace wiki
|
|
155
|
+
|
|
156
|
+
The wiki is Tiro's auto-extracted knowledge graph (entities, concepts, and their
|
|
157
|
+
links) over your notes. These commands are **read-only** and mirror the MCP wiki
|
|
158
|
+
tools. Wiki is a paid, opt-in feature — calls against an ineligible or
|
|
159
|
+
not-activated workspace return the backend's upgrade message and a non-zero exit.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
tiro wiki workspaces # which workspaces? (guid + wiki on/off)
|
|
163
|
+
tiro wiki search "Ontology" --workspace <guid> # find pages by keyword
|
|
164
|
+
tiro wiki page <pageGuid> --workspace <guid> # body + mentions + links
|
|
165
|
+
tiro wiki mentions <pageGuid> --workspace <guid> # where the page is grounded in notes
|
|
166
|
+
tiro wiki graph <pageGuid> --mode around --workspace <guid> # neighborhood graph
|
|
167
|
+
tiro wiki graph --mode seed --type CONCEPT --workspace <guid> # overview graph (no pageGuid)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`--workspace` is optional — omit it to use the default workspace for your API
|
|
171
|
+
key. If your account belongs to multiple workspaces, run `tiro wiki workspaces`
|
|
172
|
+
first and pass the `guid` of the one whose wiki is enabled. `graph` results are
|
|
173
|
+
capped (default 50 nodes); a `truncated: true` flag signals you to narrow the
|
|
174
|
+
query.
|
|
175
|
+
|
|
153
176
|
### Connect to Claude Code (MCP)
|
|
154
177
|
```bash
|
|
155
178
|
tiro mcp install
|
|
@@ -287,7 +310,7 @@ The CLI sits on the public Tiro API alongside the MCP server, sharing the same O
|
|
|
287
310
|
|
|
288
311
|
```
|
|
289
312
|
┌──────────────────────────────┐
|
|
290
|
-
│ Tiro Backend
|
|
313
|
+
│ Tiro Backend │
|
|
291
314
|
│ /v1/external/* + /v1/mcp/* │
|
|
292
315
|
└──────────────┬────────────────┘
|
|
293
316
|
│
|
|
@@ -325,7 +348,7 @@ See [`SPEC.md`](./SPEC.md) for the full v1 design and [`CHANGELOG.md`](./CHANGEL
|
|
|
325
348
|
|
|
326
349
|
## Contributing
|
|
327
350
|
|
|
328
|
-
This repo is currently private alpha. Feedback and bug reports
|
|
351
|
+
This repo is currently private alpha. Feedback and bug reports can be sent to [yeoul@theplato.io](mailto:yeoul@theplato.io); once we open up, this section will list public contribution guidelines.
|
|
329
352
|
|
|
330
353
|
---
|
|
331
354
|
|
package/dist/bin/tiro.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin/tiro.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command19 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/lib/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -43,6 +43,7 @@ var TiroError = class extends Error {
|
|
|
43
43
|
errorType;
|
|
44
44
|
httpStatus;
|
|
45
45
|
requestId;
|
|
46
|
+
details;
|
|
46
47
|
exitCode;
|
|
47
48
|
constructor(payload, exitCode = ExitCode.Generic) {
|
|
48
49
|
super(payload.message);
|
|
@@ -52,6 +53,7 @@ var TiroError = class extends Error {
|
|
|
52
53
|
this.errorType = payload.errorType;
|
|
53
54
|
this.httpStatus = payload.httpStatus;
|
|
54
55
|
this.requestId = payload.requestId;
|
|
56
|
+
this.details = payload.details;
|
|
55
57
|
this.exitCode = exitCode;
|
|
56
58
|
}
|
|
57
59
|
toJSON() {
|
|
@@ -63,7 +65,8 @@ var TiroError = class extends Error {
|
|
|
63
65
|
...this.suggestion !== void 0 && { suggestion: this.suggestion },
|
|
64
66
|
...this.errorType !== void 0 && { errorType: this.errorType },
|
|
65
67
|
...this.httpStatus !== void 0 && { httpStatus: this.httpStatus },
|
|
66
|
-
...this.requestId !== void 0 && { requestId: this.requestId }
|
|
68
|
+
...this.requestId !== void 0 && { requestId: this.requestId },
|
|
69
|
+
...this.details !== void 0 && { details: this.details }
|
|
67
70
|
}
|
|
68
71
|
};
|
|
69
72
|
}
|
|
@@ -461,6 +464,7 @@ var config = new Conf({
|
|
|
461
464
|
oauthClientId: null,
|
|
462
465
|
oauthClientIdRegisteredAt: null,
|
|
463
466
|
oauthClientHostname: null,
|
|
467
|
+
oauthClientRedirectUri: null,
|
|
464
468
|
defaultOutputDir: null
|
|
465
469
|
}
|
|
466
470
|
});
|
|
@@ -518,25 +522,40 @@ function getHostname(override) {
|
|
|
518
522
|
if (env) return validateHostname(env);
|
|
519
523
|
return validateHostname(config.get("hostname"));
|
|
520
524
|
}
|
|
521
|
-
function getOauthClientId(hostname) {
|
|
525
|
+
function getOauthClientId(hostname, currentRedirectUri) {
|
|
522
526
|
const id = config.get("oauthClientId");
|
|
523
527
|
const registeredAt = config.get("oauthClientIdRegisteredAt");
|
|
524
528
|
const cachedHostname = config.get("oauthClientHostname");
|
|
529
|
+
const cachedRedirectUri = config.get("oauthClientRedirectUri");
|
|
525
530
|
if (!id || !registeredAt) return null;
|
|
526
531
|
if (cachedHostname !== hostname) return null;
|
|
532
|
+
if (!cachedRedirectUri) return null;
|
|
533
|
+
if (!sameRedirectTarget(cachedRedirectUri, currentRedirectUri)) return null;
|
|
527
534
|
const ttlMs = 29 * 24 * 60 * 60 * 1e3;
|
|
528
535
|
if (Date.now() - registeredAt > ttlMs) return null;
|
|
529
536
|
return id;
|
|
530
537
|
}
|
|
531
|
-
function setOauthClientId(clientId, hostname) {
|
|
538
|
+
function setOauthClientId(clientId, hostname, redirectUri) {
|
|
532
539
|
config.set("oauthClientId", clientId);
|
|
533
540
|
config.set("oauthClientIdRegisteredAt", Date.now());
|
|
534
541
|
config.set("oauthClientHostname", hostname);
|
|
542
|
+
config.set("oauthClientRedirectUri", redirectUri);
|
|
535
543
|
}
|
|
536
544
|
function clearOauthClientId() {
|
|
537
545
|
config.set("oauthClientId", null);
|
|
538
546
|
config.set("oauthClientIdRegisteredAt", null);
|
|
539
547
|
config.set("oauthClientHostname", null);
|
|
548
|
+
config.set("oauthClientRedirectUri", null);
|
|
549
|
+
}
|
|
550
|
+
function sameRedirectTarget(a, b) {
|
|
551
|
+
let pa, pb;
|
|
552
|
+
try {
|
|
553
|
+
pa = new URL(a);
|
|
554
|
+
pb = new URL(b);
|
|
555
|
+
} catch {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
return pa.protocol === pb.protocol && pa.hostname === pb.hostname && pa.pathname === pb.pathname;
|
|
540
559
|
}
|
|
541
560
|
function stripTrailingSlash(s) {
|
|
542
561
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
@@ -617,7 +636,7 @@ ${authorizeUrl}`);
|
|
|
617
636
|
}
|
|
618
637
|
}
|
|
619
638
|
async function ensureOauthClient(hostname, redirectUri) {
|
|
620
|
-
const cached = getOauthClientId(hostname);
|
|
639
|
+
const cached = getOauthClientId(hostname, redirectUri);
|
|
621
640
|
if (cached) return cached;
|
|
622
641
|
const url = `${hostname}/v1/mcp/oauth/register`;
|
|
623
642
|
let res;
|
|
@@ -670,7 +689,7 @@ async function ensureOauthClient(hostname, redirectUri) {
|
|
|
670
689
|
ExitCode.Generic
|
|
671
690
|
);
|
|
672
691
|
}
|
|
673
|
-
setOauthClientId(parsed.data.client_id, hostname);
|
|
692
|
+
setOauthClientId(parsed.data.client_id, hostname, redirectUri);
|
|
674
693
|
return parsed.data.client_id;
|
|
675
694
|
}
|
|
676
695
|
function buildAuthorizeUrl(input) {
|
|
@@ -971,6 +990,116 @@ var ApiErrorSchema = z2.object({
|
|
|
971
990
|
detail: z2.string().nullable().optional()
|
|
972
991
|
})
|
|
973
992
|
});
|
|
993
|
+
var WorkspaceMeSchema = z2.object({
|
|
994
|
+
guid: z2.string()
|
|
995
|
+
}).passthrough();
|
|
996
|
+
var WorkspaceItemSchema = z2.object({
|
|
997
|
+
guid: z2.string(),
|
|
998
|
+
name: z2.string(),
|
|
999
|
+
isWikiEnabled: z2.boolean()
|
|
1000
|
+
}).passthrough();
|
|
1001
|
+
var WorkspacesListSchema = z2.object({
|
|
1002
|
+
workspaces: z2.array(WorkspaceItemSchema)
|
|
1003
|
+
}).passthrough();
|
|
1004
|
+
var WikiSearchPageSchema = z2.object({
|
|
1005
|
+
guid: z2.string(),
|
|
1006
|
+
wikiId: z2.number(),
|
|
1007
|
+
canonicalName: z2.string(),
|
|
1008
|
+
pageType: z2.string(),
|
|
1009
|
+
entitySubtype: z2.string().nullable().optional(),
|
|
1010
|
+
score: z2.number()
|
|
1011
|
+
}).passthrough();
|
|
1012
|
+
var WikiSearchPagesResponseSchema = z2.object({
|
|
1013
|
+
items: z2.array(WikiSearchPageSchema)
|
|
1014
|
+
}).passthrough();
|
|
1015
|
+
var WikiMentionSchema = z2.object({
|
|
1016
|
+
guid: z2.string(),
|
|
1017
|
+
noteId: z2.number(),
|
|
1018
|
+
paragraphId: z2.number().nullable().optional(),
|
|
1019
|
+
paragraphUuid: z2.string().nullable().optional(),
|
|
1020
|
+
kind: z2.string(),
|
|
1021
|
+
sourceUserId: z2.number(),
|
|
1022
|
+
extractedText: z2.string(),
|
|
1023
|
+
confidence: z2.number().nullable().optional(),
|
|
1024
|
+
createdAt: z2.string()
|
|
1025
|
+
}).passthrough();
|
|
1026
|
+
var WikiAliasSchema = z2.object({
|
|
1027
|
+
guid: z2.string(),
|
|
1028
|
+
alias: z2.string(),
|
|
1029
|
+
source: z2.string(),
|
|
1030
|
+
sourceMentionGuid: z2.string().nullable().optional(),
|
|
1031
|
+
sourceUserId: z2.number().nullable().optional(),
|
|
1032
|
+
createdAt: z2.string()
|
|
1033
|
+
}).passthrough();
|
|
1034
|
+
var WikiLinkSchema = z2.object({
|
|
1035
|
+
guid: z2.string(),
|
|
1036
|
+
sourcePageGuid: z2.string(),
|
|
1037
|
+
sourcePageName: z2.string().nullable().optional(),
|
|
1038
|
+
targetPageGuid: z2.string(),
|
|
1039
|
+
targetPageName: z2.string().nullable().optional(),
|
|
1040
|
+
linkType: z2.string(),
|
|
1041
|
+
linkTypeDisplayKo: z2.string().optional(),
|
|
1042
|
+
linkTypeDisplayEn: z2.string().optional(),
|
|
1043
|
+
isDirectional: z2.boolean(),
|
|
1044
|
+
source: z2.string().optional(),
|
|
1045
|
+
creatorUserId: z2.number().nullable().optional(),
|
|
1046
|
+
createdAt: z2.string().optional(),
|
|
1047
|
+
updatedAt: z2.string().optional()
|
|
1048
|
+
}).passthrough();
|
|
1049
|
+
var WikiPageDetailSchema = z2.object({
|
|
1050
|
+
guid: z2.string(),
|
|
1051
|
+
wikiId: z2.number(),
|
|
1052
|
+
canonicalName: z2.string(),
|
|
1053
|
+
description: z2.string().nullable().optional(),
|
|
1054
|
+
descriptionStatus: z2.string().nullable().optional(),
|
|
1055
|
+
regenerationAvailable: z2.boolean().nullable().optional(),
|
|
1056
|
+
pageType: z2.string(),
|
|
1057
|
+
entitySubtype: z2.string().nullable().optional(),
|
|
1058
|
+
extractionStatus: z2.string(),
|
|
1059
|
+
mentionCountVisible: z2.number(),
|
|
1060
|
+
mentions: z2.array(WikiMentionSchema),
|
|
1061
|
+
aliases: z2.array(WikiAliasSchema),
|
|
1062
|
+
links: z2.array(WikiLinkSchema),
|
|
1063
|
+
lastUpdatedVisible: z2.string().nullable().optional(),
|
|
1064
|
+
createdAt: z2.string(),
|
|
1065
|
+
updatedAt: z2.string()
|
|
1066
|
+
}).passthrough();
|
|
1067
|
+
var WikiMentionListResponseSchema = z2.object({
|
|
1068
|
+
items: z2.array(WikiMentionSchema),
|
|
1069
|
+
nextCursorCreatedAt: z2.string().nullable(),
|
|
1070
|
+
nextCursorId: z2.number().nullable()
|
|
1071
|
+
}).passthrough();
|
|
1072
|
+
var WikiGraphNodeSchema = z2.object({
|
|
1073
|
+
guid: z2.string(),
|
|
1074
|
+
canonicalName: z2.string(),
|
|
1075
|
+
pageType: z2.string(),
|
|
1076
|
+
entitySubtype: z2.string().nullable().optional(),
|
|
1077
|
+
extractionStatus: z2.string(),
|
|
1078
|
+
mentionCountVisible: z2.number()
|
|
1079
|
+
}).passthrough();
|
|
1080
|
+
var WikiGraphEdgeSchema = z2.object({
|
|
1081
|
+
guid: z2.string(),
|
|
1082
|
+
sourcePageGuid: z2.string(),
|
|
1083
|
+
targetPageGuid: z2.string(),
|
|
1084
|
+
linkType: z2.string(),
|
|
1085
|
+
linkTypeDisplayKo: z2.string().optional(),
|
|
1086
|
+
linkTypeDisplayEn: z2.string().optional(),
|
|
1087
|
+
isDirectional: z2.boolean()
|
|
1088
|
+
}).passthrough();
|
|
1089
|
+
var WikiGraphResponseSchema = z2.object({
|
|
1090
|
+
nodes: z2.array(WikiGraphNodeSchema),
|
|
1091
|
+
edges: z2.array(WikiGraphEdgeSchema)
|
|
1092
|
+
}).passthrough();
|
|
1093
|
+
var WikiPlanRequiredSchema = z2.object({
|
|
1094
|
+
// Two gate kinds, relayed verbatim:
|
|
1095
|
+
// WIKI_PLAN_REQUIRED → plan ineligible (upsell; required_plans set)
|
|
1096
|
+
// WIKI_NOT_ACTIVATED → eligible plan, wiki not activated by an admin (required_plans null)
|
|
1097
|
+
error_code: z2.enum(["WIKI_PLAN_REQUIRED", "WIKI_NOT_ACTIVATED"]),
|
|
1098
|
+
message: z2.string(),
|
|
1099
|
+
current_plan: z2.string().nullable().optional(),
|
|
1100
|
+
required_plans: z2.array(z2.string()).nullable().optional(),
|
|
1101
|
+
action_url: z2.string().nullable().optional()
|
|
1102
|
+
}).passthrough();
|
|
974
1103
|
|
|
975
1104
|
// src/lib/api/client.ts
|
|
976
1105
|
var TiroApiClient = class {
|
|
@@ -1062,6 +1191,10 @@ var TiroApiClient = class {
|
|
|
1062
1191
|
async function mapHttpError(res, method, path) {
|
|
1063
1192
|
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
1064
1193
|
const exitCode = res.status === 401 ? ExitCode.AuthRequired : ExitCode.Generic;
|
|
1194
|
+
if (res.status === 402) {
|
|
1195
|
+
const gate = await tryParsePlanGate(res);
|
|
1196
|
+
if (gate) return planGateError(gate, requestId);
|
|
1197
|
+
}
|
|
1065
1198
|
const apiError = await tryParseApiError(res);
|
|
1066
1199
|
if (apiError) {
|
|
1067
1200
|
return new TiroError(
|
|
@@ -1092,6 +1225,7 @@ async function mapHttpError(res, method, path) {
|
|
|
1092
1225
|
function httpStatusToErrorType(status) {
|
|
1093
1226
|
if (status === 400) return "bad_request";
|
|
1094
1227
|
if (status === 401) return "unauthorized";
|
|
1228
|
+
if (status === 402) return "payment_required";
|
|
1095
1229
|
if (status === 403) return "forbidden";
|
|
1096
1230
|
if (status === 404) return "not_found";
|
|
1097
1231
|
if (status === 409) return "conflict";
|
|
@@ -1109,6 +1243,31 @@ async function tryParseApiError(res) {
|
|
|
1109
1243
|
return null;
|
|
1110
1244
|
}
|
|
1111
1245
|
}
|
|
1246
|
+
async function tryParsePlanGate(res) {
|
|
1247
|
+
try {
|
|
1248
|
+
const json = await res.clone().json();
|
|
1249
|
+
const parsed = WikiPlanRequiredSchema.safeParse(json);
|
|
1250
|
+
return parsed.success ? parsed.data : null;
|
|
1251
|
+
} catch {
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function planGateError(gate, requestId) {
|
|
1256
|
+
const actionUrl = gate.action_url ?? null;
|
|
1257
|
+
return new TiroError(
|
|
1258
|
+
{
|
|
1259
|
+
// Relay the backend's discriminator (WIKI_PLAN_REQUIRED / WIKI_NOT_ACTIVATED) verbatim.
|
|
1260
|
+
code: gate.error_code,
|
|
1261
|
+
message: gate.message,
|
|
1262
|
+
errorType: "payment_required",
|
|
1263
|
+
httpStatus: 402,
|
|
1264
|
+
...actionUrl ? { suggestion: `Upgrade: ${actionUrl}` } : {},
|
|
1265
|
+
...requestId !== void 0 && { requestId },
|
|
1266
|
+
details: { ...gate }
|
|
1267
|
+
},
|
|
1268
|
+
ExitCode.Generic
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1112
1271
|
function buildApiUrl(hostname, path, params) {
|
|
1113
1272
|
if (!path.startsWith("/") || path.startsWith("//") || path.startsWith("/\\")) {
|
|
1114
1273
|
throw new TiroError(
|
|
@@ -1198,17 +1357,18 @@ Examples:
|
|
|
1198
1357
|
tiro notes list --folder <id> --limit 50
|
|
1199
1358
|
|
|
1200
1359
|
Keyword matching:
|
|
1201
|
-
--keyword reorders results by
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
by
|
|
1360
|
+
--keyword reorders results by full-text relevance over title and
|
|
1361
|
+
paragraph content (case-insensitive), with createdAt desc as tiebreaker
|
|
1362
|
+
when scores are close. When --keyword is set, nextCursor is always null.
|
|
1363
|
+
Without --keyword, results are ordered by recording time desc (falls back
|
|
1364
|
+
to createdAt for memo-only notes).
|
|
1205
1365
|
|
|
1206
1366
|
Note: placeholder notes (title='Untitled' or sourceType='onboarding') are
|
|
1207
1367
|
filtered out by default. A page of N may return fewer than N visible notes \u2014
|
|
1208
1368
|
keep paginating to fetch more, or pass --include-untitled to surface them.
|
|
1209
1369
|
`;
|
|
1210
1370
|
function registerNotesList(parent) {
|
|
1211
|
-
parent.command("list").description("List notes (lightweight metadata).").option("--keyword <text>", 'Reorder by
|
|
1371
|
+
parent.command("list").description("List notes (lightweight metadata).").option("--keyword <text>", 'Reorder by full-text relevance for this keyword (e.g. "OKR")').option("--folder <id>", "Restrict to a folder and its descendants").option(
|
|
1212
1372
|
"--since <date>",
|
|
1213
1373
|
"Inclusive lower bound on createdAt (ISO-8601 or relative: 7d, 24h, 30m)"
|
|
1214
1374
|
).option("--until <date>", "Exclusive upper bound on createdAt").option(
|
|
@@ -1248,7 +1408,7 @@ function clampLimit(raw) {
|
|
|
1248
1408
|
if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE;
|
|
1249
1409
|
return Math.min(n, MAX_PAGE_SIZE);
|
|
1250
1410
|
}
|
|
1251
|
-
function printPretty(notes,
|
|
1411
|
+
function printPretty(notes, nextCursor2, opts) {
|
|
1252
1412
|
if (notes.length === 0) {
|
|
1253
1413
|
process.stdout.write(`${color("(no notes)", "gray", opts)}
|
|
1254
1414
|
`);
|
|
@@ -1264,10 +1424,10 @@ function printPretty(notes, nextCursor, opts) {
|
|
|
1264
1424
|
`
|
|
1265
1425
|
);
|
|
1266
1426
|
}
|
|
1267
|
-
if (
|
|
1427
|
+
if (nextCursor2) {
|
|
1268
1428
|
process.stdout.write(
|
|
1269
1429
|
`${color(`
|
|
1270
|
-
next: --cursor ${
|
|
1430
|
+
next: --cursor ${nextCursor2}`, "gray", opts)}
|
|
1271
1431
|
`
|
|
1272
1432
|
);
|
|
1273
1433
|
}
|
|
@@ -1307,12 +1467,16 @@ Examples:
|
|
|
1307
1467
|
tiro notes search "release" --since 2026-04-01 --until 2026-05-01
|
|
1308
1468
|
|
|
1309
1469
|
Keyword matching:
|
|
1310
|
-
Full-text against note title + paragraph content
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1470
|
+
Full-text search against note title + paragraph content. Case-insensitive.
|
|
1471
|
+
Multi-word keywords are tokenized \u2014 "OKR planning" matches notes containing
|
|
1472
|
+
both terms. The deep search variant (this command) hydrates each result
|
|
1473
|
+
with its primary documents (one-pager, custom) so an MCP/LLM client can
|
|
1474
|
+
read content alongside metadata in one call.
|
|
1475
|
+
|
|
1476
|
+
Ordering:
|
|
1477
|
+
Results are sorted by full-text relevance, then createdAt desc as
|
|
1478
|
+
tiebreaker. Without --limit, the server returns 50 results (capped
|
|
1479
|
+
at 200). Pass --limit to override.
|
|
1316
1480
|
|
|
1317
1481
|
Note: placeholder notes (title='Untitled' or sourceType='onboarding') are
|
|
1318
1482
|
filtered out by default. Pass --include-untitled to surface them.
|
|
@@ -1392,7 +1556,7 @@ function clampLimit2(raw) {
|
|
|
1392
1556
|
if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE2;
|
|
1393
1557
|
return Math.min(n, MAX_PAGE_SIZE2);
|
|
1394
1558
|
}
|
|
1395
|
-
function printPretty2(notes,
|
|
1559
|
+
function printPretty2(notes, nextCursor2, opts) {
|
|
1396
1560
|
if (notes.length === 0) {
|
|
1397
1561
|
process.stdout.write(`${color("(no matches)", "gray", opts)}
|
|
1398
1562
|
`);
|
|
@@ -1405,10 +1569,10 @@ function printPretty2(notes, nextCursor, opts) {
|
|
|
1405
1569
|
`
|
|
1406
1570
|
);
|
|
1407
1571
|
}
|
|
1408
|
-
if (
|
|
1572
|
+
if (nextCursor2) {
|
|
1409
1573
|
process.stdout.write(
|
|
1410
1574
|
`${color(`
|
|
1411
|
-
next: --cursor ${
|
|
1575
|
+
next: --cursor ${nextCursor2}`, "gray", opts)}
|
|
1412
1576
|
`
|
|
1413
1577
|
);
|
|
1414
1578
|
}
|
|
@@ -1902,6 +2066,413 @@ function registerNotes(program) {
|
|
|
1902
2066
|
registerNotesTranscript(notes);
|
|
1903
2067
|
}
|
|
1904
2068
|
|
|
2069
|
+
// src/commands/wiki/index.ts
|
|
2070
|
+
import "commander";
|
|
2071
|
+
|
|
2072
|
+
// src/commands/wiki/search.ts
|
|
2073
|
+
import "commander";
|
|
2074
|
+
|
|
2075
|
+
// src/lib/api/workspace.ts
|
|
2076
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
2077
|
+
async function resolveWorkspaceGuid(client) {
|
|
2078
|
+
const cached = cache.get(client);
|
|
2079
|
+
if (cached) return cached;
|
|
2080
|
+
const promise = client.getJson("/v1/external/workspaces/me", WorkspaceMeSchema).then((res) => res.guid);
|
|
2081
|
+
cache.set(client, promise);
|
|
2082
|
+
return promise;
|
|
2083
|
+
}
|
|
2084
|
+
async function listWorkspaces(client) {
|
|
2085
|
+
const res = await client.getJson("/v1/external/workspaces", WorkspacesListSchema);
|
|
2086
|
+
return res.workspaces;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/commands/wiki/search.ts
|
|
2090
|
+
var MAX_SIZE = 100;
|
|
2091
|
+
var MAX_QUERY_LENGTH = 500;
|
|
2092
|
+
var HELP_AFTER3 = `
|
|
2093
|
+
Examples:
|
|
2094
|
+
tiro wiki search "onboarding"
|
|
2095
|
+
tiro wiki search "payment gateway" --size 50 --json
|
|
2096
|
+
tiro wiki search "billing" --workspace <guid>
|
|
2097
|
+
|
|
2098
|
+
The workspace defaults to the one implicit in your API key. Pass --workspace
|
|
2099
|
+
to target a specific workspace (get guids from 'tiro wiki workspaces').
|
|
2100
|
+
Results are ranked wiki pages (pageGuid, name, type, relevance). Pass a
|
|
2101
|
+
pageGuid to 'tiro wiki page', 'tiro wiki mentions', or 'tiro wiki graph'.
|
|
2102
|
+
`;
|
|
2103
|
+
function registerWikiSearch(parent) {
|
|
2104
|
+
parent.command("search <query>").description("Search the workspace wiki for pages matching a keyword.").option("--size <n>", `Max results (default: backend default, max ${MAX_SIZE})`).option("--workspace <guid>", "Target a specific workspace (from `tiro wiki workspaces`); defaults to your default workspace").addHelpText("after", HELP_AFTER3).action(async (query, opts, cmd) => {
|
|
2105
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2106
|
+
const keyword = query.trim();
|
|
2107
|
+
if (!keyword) {
|
|
2108
|
+
throw new TiroError(
|
|
2109
|
+
{
|
|
2110
|
+
code: "missing_query",
|
|
2111
|
+
message: "wiki search requires a query.",
|
|
2112
|
+
errorType: "bad_request",
|
|
2113
|
+
suggestion: 'tiro wiki search "onboarding"'
|
|
2114
|
+
},
|
|
2115
|
+
ExitCode.Usage
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
const client = createApiClient({
|
|
2119
|
+
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
2120
|
+
});
|
|
2121
|
+
const workspaceGuid = opts.workspace ?? await resolveWorkspaceGuid(client);
|
|
2122
|
+
const clampedKeyword = keyword.length > MAX_QUERY_LENGTH ? keyword.slice(0, MAX_QUERY_LENGTH) : keyword;
|
|
2123
|
+
const params = { q: clampedKeyword };
|
|
2124
|
+
const size = clampSize(opts.size);
|
|
2125
|
+
if (size !== void 0) params["size"] = size;
|
|
2126
|
+
const res = await client.getJson(
|
|
2127
|
+
// Fix 1: encode workspaceGuid so path-traversal sequences (../, ?, #) are neutralised.
|
|
2128
|
+
`/v1/external/workspaces/${encodeURIComponent(workspaceGuid)}/wiki/search/pages`,
|
|
2129
|
+
WikiSearchPagesResponseSchema,
|
|
2130
|
+
params
|
|
2131
|
+
);
|
|
2132
|
+
const mode = resolveOutputMode(globalOpts);
|
|
2133
|
+
if (mode === "json") {
|
|
2134
|
+
for (const item of res.items) printNdjson(item);
|
|
2135
|
+
} else {
|
|
2136
|
+
printPretty3(res.items, globalOpts);
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
function clampSize(raw) {
|
|
2141
|
+
if (!raw) return void 0;
|
|
2142
|
+
const n = parseInt(raw, 10);
|
|
2143
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
2144
|
+
return Math.min(n, MAX_SIZE);
|
|
2145
|
+
}
|
|
2146
|
+
function printPretty3(items, opts) {
|
|
2147
|
+
if (items.length === 0) {
|
|
2148
|
+
process.stdout.write(`${color("(no matches)", "gray", opts)}
|
|
2149
|
+
`);
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
for (const it of items) {
|
|
2153
|
+
const score = it.score.toFixed(2);
|
|
2154
|
+
process.stdout.write(
|
|
2155
|
+
`${color(score, "cyan", opts)} ${color(it.guid, "dim", opts)} ${color(it.pageType, "gray", opts)} ${it.canonicalName}
|
|
2156
|
+
`
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// src/commands/wiki/page.ts
|
|
2162
|
+
import "commander";
|
|
2163
|
+
var HELP_AFTER4 = `
|
|
2164
|
+
Examples:
|
|
2165
|
+
tiro wiki page <pageGuid>
|
|
2166
|
+
tiro wiki page <pageGuid> --json
|
|
2167
|
+
tiro wiki page <pageGuid> --workspace <guid>
|
|
2168
|
+
|
|
2169
|
+
Returns the page body (per-user description) plus metadata, mentions,
|
|
2170
|
+
aliases, and links. The workspace defaults to the one implicit in your API
|
|
2171
|
+
key; pass --workspace to target a specific workspace (from 'tiro wiki workspaces').
|
|
2172
|
+
`;
|
|
2173
|
+
function registerWikiPage(parent) {
|
|
2174
|
+
parent.command("page <pageGuid>").description("Get a single wiki page: body, metadata, mentions, aliases, and links.").option("--workspace <guid>", "Target a specific workspace (from `tiro wiki workspaces`); defaults to your default workspace").addHelpText("after", HELP_AFTER4).action(async (pageGuid, opts, cmd) => {
|
|
2175
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2176
|
+
const client = createApiClient({
|
|
2177
|
+
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
2178
|
+
});
|
|
2179
|
+
const workspaceGuid = opts.workspace ?? await resolveWorkspaceGuid(client);
|
|
2180
|
+
const page = await client.getJson(
|
|
2181
|
+
`/v1/external/workspaces/${encodeURIComponent(workspaceGuid)}/wiki/pages/${encodeURIComponent(pageGuid)}`,
|
|
2182
|
+
WikiPageDetailSchema
|
|
2183
|
+
);
|
|
2184
|
+
const mode = resolveOutputMode(globalOpts);
|
|
2185
|
+
if (mode === "json") {
|
|
2186
|
+
printOutput({ ok: true, data: page }, globalOpts);
|
|
2187
|
+
} else {
|
|
2188
|
+
printPretty4(page, globalOpts);
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
function printPretty4(page, opts) {
|
|
2193
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
2194
|
+
w(`${color(page.canonicalName, "bold", opts)} ${color(`(${page.pageType})`, "gray", opts)}
|
|
2195
|
+
`);
|
|
2196
|
+
w(`${color(page.guid, "dim", opts)}
|
|
2197
|
+
|
|
2198
|
+
`);
|
|
2199
|
+
if (page.description) {
|
|
2200
|
+
w(`${page.description}
|
|
2201
|
+
|
|
2202
|
+
`);
|
|
2203
|
+
} else {
|
|
2204
|
+
const status = page.descriptionStatus ?? "none";
|
|
2205
|
+
w(`${color(`(no description \u2014 status: ${status})`, "gray", opts)}
|
|
2206
|
+
|
|
2207
|
+
`);
|
|
2208
|
+
}
|
|
2209
|
+
w(
|
|
2210
|
+
`${color("mentions", "gray", opts)} ${page.mentionCountVisible} ${color("aliases", "gray", opts)} ${page.aliases.length} ${color("links", "gray", opts)} ${page.links.length}
|
|
2211
|
+
`
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// src/commands/wiki/mentions.ts
|
|
2216
|
+
import "commander";
|
|
2217
|
+
var MAX_LIMIT = 200;
|
|
2218
|
+
var HELP_AFTER5 = `
|
|
2219
|
+
Examples:
|
|
2220
|
+
tiro wiki mentions <pageGuid>
|
|
2221
|
+
tiro wiki mentions <pageGuid> --limit 100 --json
|
|
2222
|
+
tiro wiki mentions <pageGuid> --workspace <guid>
|
|
2223
|
+
|
|
2224
|
+
Lists the note paragraphs that reference a wiki page. In JSON mode a trailing
|
|
2225
|
+
{_cursor: ...} object is emitted when more pages are available; pass the
|
|
2226
|
+
cursor token back via --cursor to fetch the next page. The workspace defaults
|
|
2227
|
+
to the one implicit in your API key; pass --workspace to target a specific
|
|
2228
|
+
workspace (from 'tiro wiki workspaces').
|
|
2229
|
+
`;
|
|
2230
|
+
function registerWikiMentions(parent) {
|
|
2231
|
+
parent.command("mentions <pageGuid>").description("List the note mentions that reference a single wiki page.").option("--limit <n>", `Max mentions per page (max ${MAX_LIMIT})`).option("--cursor <token>", "Continue from a previous page's cursor token.").option("--workspace <guid>", "Target a specific workspace (from `tiro wiki workspaces`); defaults to your default workspace").addHelpText("after", HELP_AFTER5).action(async (pageGuid, opts, cmd) => {
|
|
2232
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2233
|
+
const client = createApiClient({
|
|
2234
|
+
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
2235
|
+
});
|
|
2236
|
+
const workspaceGuid = opts.workspace ?? await resolveWorkspaceGuid(client);
|
|
2237
|
+
const params = {};
|
|
2238
|
+
const limit = clampLimit3(opts.limit);
|
|
2239
|
+
if (limit !== void 0) params["limit"] = limit;
|
|
2240
|
+
if (opts.cursor) params["cursor"] = opts.cursor;
|
|
2241
|
+
const res = await client.getJson(
|
|
2242
|
+
`/v1/external/workspaces/${encodeURIComponent(workspaceGuid)}/wiki/pages/${encodeURIComponent(pageGuid)}/mentions`,
|
|
2243
|
+
WikiMentionListResponseSchema,
|
|
2244
|
+
params
|
|
2245
|
+
);
|
|
2246
|
+
const mode = resolveOutputMode(globalOpts);
|
|
2247
|
+
if (mode === "json") {
|
|
2248
|
+
for (const item of res.items) printNdjson(item);
|
|
2249
|
+
const next = nextCursor(res.nextCursorCreatedAt, res.nextCursorId);
|
|
2250
|
+
if (next) printNdjson({ _cursor: next });
|
|
2251
|
+
} else {
|
|
2252
|
+
printPretty5(res.items, globalOpts);
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
function clampLimit3(raw) {
|
|
2257
|
+
if (!raw) return void 0;
|
|
2258
|
+
const n = parseInt(raw, 10);
|
|
2259
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
2260
|
+
return Math.min(n, MAX_LIMIT);
|
|
2261
|
+
}
|
|
2262
|
+
function nextCursor(createdAt, id) {
|
|
2263
|
+
if (createdAt === null || id === null) return null;
|
|
2264
|
+
const millis = Date.parse(createdAt);
|
|
2265
|
+
if (Number.isNaN(millis)) return null;
|
|
2266
|
+
return `${millis}_${id}`;
|
|
2267
|
+
}
|
|
2268
|
+
function printPretty5(items, opts) {
|
|
2269
|
+
if (items.length === 0) {
|
|
2270
|
+
process.stdout.write(`${color("(no mentions)", "gray", opts)}
|
|
2271
|
+
`);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
for (const m of items) {
|
|
2275
|
+
const date = m.createdAt.slice(0, 10);
|
|
2276
|
+
process.stdout.write(
|
|
2277
|
+
`${color(date, "gray", opts)} ${color(m.kind, "cyan", opts)} ${m.extractedText}
|
|
2278
|
+
`
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/commands/wiki/graph.ts
|
|
2284
|
+
import "commander";
|
|
2285
|
+
var MODES = ["seed", "expand", "around", "links"];
|
|
2286
|
+
var MAX_QUERY_LENGTH2 = 500;
|
|
2287
|
+
var GRAPH_CAP_DEFAULT = 50;
|
|
2288
|
+
var HELP_AFTER6 = `
|
|
2289
|
+
Modes (--mode, default: around):
|
|
2290
|
+
around Neighborhood around <pageGuid> (--radius hops, default 2)
|
|
2291
|
+
expand Outward expansion from <pageGuid> (--depth hops, default 1)
|
|
2292
|
+
links Links among <pageGuid> + each --page <guid> (repeatable)
|
|
2293
|
+
seed Overview seed graph (no <pageGuid> required; --query, --type, --since)
|
|
2294
|
+
|
|
2295
|
+
Examples:
|
|
2296
|
+
tiro wiki graph <pageGuid> # around, radius 2
|
|
2297
|
+
tiro wiki graph <pageGuid> --mode expand --depth 2
|
|
2298
|
+
tiro wiki graph <pageGuid> --mode links --page <g2> --page <g3>
|
|
2299
|
+
tiro wiki graph --mode seed --query "billing"
|
|
2300
|
+
tiro wiki graph <pageGuid> --workspace <guid>
|
|
2301
|
+
|
|
2302
|
+
Returns a node+edge slice of the workspace wiki graph. The workspace defaults
|
|
2303
|
+
to the one implicit in your API key; pass --workspace to target a specific
|
|
2304
|
+
workspace (from 'tiro wiki workspaces').
|
|
2305
|
+
`;
|
|
2306
|
+
function registerWikiGraph(parent) {
|
|
2307
|
+
parent.command("graph [pageGuid]").description("Get a node+edge slice of the wiki link graph around a page.").option("--mode <mode>", `Graph mode: ${MODES.join(" | ")} (default: around)`).option("--depth <n>", "expand mode: link-hops to traverse outward (default 1)").option("--radius <n>", "around mode: neighborhood radius in hops (default 2)").option("--limit <n>", "Max nodes to return (max 200)").option("--page <guid>", "links mode: additional page guid (repeatable)", collect, []).option("--type <type>", "seed mode: restrict to a page type (CONCEPT|DECISION|ENTITY)").option("--since <date>", "seed mode: only pages updated at/after this date").option("--query <keyword>", "seed mode: keyword to seed the overview graph").option("--workspace <guid>", "Target a specific workspace (from `tiro wiki workspaces`); defaults to your default workspace").addHelpText("after", HELP_AFTER6).action(async (pageGuid, opts, cmd) => {
|
|
2308
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2309
|
+
const mode = parseMode(opts.mode);
|
|
2310
|
+
if ((mode === "expand" || mode === "around") && !pageGuid) {
|
|
2311
|
+
throw new TiroError(
|
|
2312
|
+
{
|
|
2313
|
+
code: "missing_page_guid",
|
|
2314
|
+
message: `--mode ${mode} requires a <pageGuid> positional argument.`,
|
|
2315
|
+
errorType: "bad_request",
|
|
2316
|
+
suggestion: `tiro wiki graph <pageGuid> --mode ${mode}`
|
|
2317
|
+
},
|
|
2318
|
+
ExitCode.Usage
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
const client = createApiClient({
|
|
2322
|
+
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
2323
|
+
});
|
|
2324
|
+
const workspaceGuid = opts.workspace ?? await resolveWorkspaceGuid(client);
|
|
2325
|
+
const effectiveLimit = mode === "links" ? void 0 : clampInt(opts.limit, 200) ?? GRAPH_CAP_DEFAULT;
|
|
2326
|
+
const raw = await fetchGraph(client, workspaceGuid, mode, pageGuid ?? "", opts, effectiveLimit);
|
|
2327
|
+
const res = applyGraphCap(raw, effectiveLimit ?? raw.nodes.length);
|
|
2328
|
+
const mode2 = resolveOutputMode(globalOpts);
|
|
2329
|
+
if (mode2 === "json") {
|
|
2330
|
+
printOutput({ ok: true, data: res }, globalOpts);
|
|
2331
|
+
} else {
|
|
2332
|
+
printPretty6(res, globalOpts);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
async function fetchGraph(client, workspaceGuid, mode, pageGuid, opts, limit) {
|
|
2337
|
+
const base = `/v1/external/workspaces/${encodeURIComponent(workspaceGuid)}/wiki/graph`;
|
|
2338
|
+
if (mode === "expand") {
|
|
2339
|
+
return client.getJson(`${base}/expand`, WikiGraphResponseSchema, {
|
|
2340
|
+
pageGuid,
|
|
2341
|
+
depth: clampInt(opts.depth, 200),
|
|
2342
|
+
limit
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
if (mode === "around") {
|
|
2346
|
+
return client.getJson(`${base}/around`, WikiGraphResponseSchema, {
|
|
2347
|
+
pageGuid,
|
|
2348
|
+
radius: clampInt(opts.radius, 200),
|
|
2349
|
+
limit
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
if (mode === "links") {
|
|
2353
|
+
const guids = [pageGuid, ...opts.page ?? []].filter((g) => g && g.length > 0);
|
|
2354
|
+
return client.getJson(`${base}/links`, WikiGraphResponseSchema, {
|
|
2355
|
+
pageGuids: guids.join(",")
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
const rawQuery = opts.query;
|
|
2359
|
+
const clampedQuery = rawQuery && rawQuery.length > MAX_QUERY_LENGTH2 ? rawQuery.slice(0, MAX_QUERY_LENGTH2) : rawQuery;
|
|
2360
|
+
return client.getJson(`${base}/seed`, WikiGraphResponseSchema, {
|
|
2361
|
+
type: opts.type,
|
|
2362
|
+
since: opts.since ? parseDate(opts.since) : void 0,
|
|
2363
|
+
q: clampedQuery,
|
|
2364
|
+
limit
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
function parseMode(raw) {
|
|
2368
|
+
if (!raw) return "around";
|
|
2369
|
+
const m = raw.toLowerCase();
|
|
2370
|
+
if (MODES.includes(m)) return m;
|
|
2371
|
+
throw new TiroError(
|
|
2372
|
+
{
|
|
2373
|
+
code: "invalid_mode",
|
|
2374
|
+
message: `Invalid --mode "${raw}". Allowed: ${MODES.join(", ")}.`,
|
|
2375
|
+
errorType: "bad_request",
|
|
2376
|
+
suggestion: "tiro wiki graph <pageGuid> --mode around"
|
|
2377
|
+
},
|
|
2378
|
+
ExitCode.Usage
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
function clampInt(raw, max) {
|
|
2382
|
+
if (!raw) return void 0;
|
|
2383
|
+
const n = parseInt(raw, 10);
|
|
2384
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
2385
|
+
return Math.min(n, max);
|
|
2386
|
+
}
|
|
2387
|
+
function collect(value, prev) {
|
|
2388
|
+
return [...prev, value];
|
|
2389
|
+
}
|
|
2390
|
+
function applyGraphCap(graph, limit) {
|
|
2391
|
+
const keptNodes = graph.nodes.slice(0, limit);
|
|
2392
|
+
const keptGuids = new Set(keptNodes.map((n) => n.guid));
|
|
2393
|
+
const keptEdges = graph.edges.filter(
|
|
2394
|
+
(e) => keptGuids.has(e.sourcePageGuid) && keptGuids.has(e.targetPageGuid)
|
|
2395
|
+
);
|
|
2396
|
+
const truncated = graph.nodes.length > limit || keptEdges.length < graph.edges.length;
|
|
2397
|
+
return { ...graph, nodes: keptNodes, edges: keptEdges, truncated };
|
|
2398
|
+
}
|
|
2399
|
+
function printPretty6(res, opts) {
|
|
2400
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
2401
|
+
w(
|
|
2402
|
+
`${color("nodes", "gray", opts)} ${res.nodes.length} ${color("edges", "gray", opts)} ${res.edges.length}
|
|
2403
|
+
`
|
|
2404
|
+
);
|
|
2405
|
+
if (res.truncated) {
|
|
2406
|
+
w(
|
|
2407
|
+
`${color(`(truncated \u2014 showing first ${res.nodes.length} nodes; narrow with --query / --type / smaller --radius)`, "gray", opts)}
|
|
2408
|
+
`
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
w("\n");
|
|
2412
|
+
for (const n of res.nodes) {
|
|
2413
|
+
w(`${color("\u25CF", "cyan", opts)} ${color(n.guid, "dim", opts)} ${n.canonicalName}
|
|
2414
|
+
`);
|
|
2415
|
+
}
|
|
2416
|
+
if (res.edges.length > 0) w("\n");
|
|
2417
|
+
for (const e of res.edges) {
|
|
2418
|
+
const arrow = e.isDirectional ? "\u2192" : "\u2014";
|
|
2419
|
+
w(
|
|
2420
|
+
`${color(e.sourcePageGuid, "dim", opts)} ${arrow} ${color(e.targetPageGuid, "dim", opts)} ${color(e.linkType, "gray", opts)}
|
|
2421
|
+
`
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// src/commands/wiki/workspaces.ts
|
|
2427
|
+
import "commander";
|
|
2428
|
+
var HELP_AFTER7 = `
|
|
2429
|
+
Examples:
|
|
2430
|
+
tiro wiki workspaces
|
|
2431
|
+
tiro wiki workspaces --json
|
|
2432
|
+
|
|
2433
|
+
Lists all workspaces you are a member of. Use the guid with --workspace on
|
|
2434
|
+
any other wiki command to target a non-default workspace.
|
|
2435
|
+
`;
|
|
2436
|
+
function registerWikiWorkspaces(parent) {
|
|
2437
|
+
parent.command("workspaces").description("List all workspaces you are a member of (use guid with --workspace).").addHelpText("after", HELP_AFTER7).action(async (_opts, cmd) => {
|
|
2438
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2439
|
+
const client = createApiClient({
|
|
2440
|
+
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
2441
|
+
});
|
|
2442
|
+
const workspaces = await listWorkspaces(client);
|
|
2443
|
+
const mode = resolveOutputMode(globalOpts);
|
|
2444
|
+
if (mode === "json") {
|
|
2445
|
+
for (const ws of workspaces) printNdjson(ws);
|
|
2446
|
+
} else {
|
|
2447
|
+
printPretty7(workspaces, globalOpts);
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
function printPretty7(workspaces, opts) {
|
|
2452
|
+
if (workspaces.length === 0) {
|
|
2453
|
+
process.stdout.write(`${color("(no workspaces)", "gray", opts)}
|
|
2454
|
+
`);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
for (const ws of workspaces) {
|
|
2458
|
+
const wikiStatus = ws.isWikiEnabled ? color("wiki:on", "green", opts) : color("wiki:off", "gray", opts);
|
|
2459
|
+
process.stdout.write(
|
|
2460
|
+
`${color(ws.guid, "dim", opts)} ${wikiStatus} ${ws.name}
|
|
2461
|
+
`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// src/commands/wiki/index.ts
|
|
2467
|
+
function registerWiki(program) {
|
|
2468
|
+
const wiki = program.command("wiki").description("Search and explore the workspace wiki (pages, mentions, link graph)");
|
|
2469
|
+
registerWikiSearch(wiki);
|
|
2470
|
+
registerWikiPage(wiki);
|
|
2471
|
+
registerWikiMentions(wiki);
|
|
2472
|
+
registerWikiGraph(wiki);
|
|
2473
|
+
registerWikiWorkspaces(wiki);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
1905
2476
|
// src/commands/mcp/index.ts
|
|
1906
2477
|
import "commander";
|
|
1907
2478
|
|
|
@@ -2024,6 +2595,8 @@ EXAMPLES
|
|
|
2024
2595
|
$ tiro notes get <guid> --output ./meeting.md --include transcript
|
|
2025
2596
|
$ tiro notes transcript <guid> --format md --output ./transcript.md
|
|
2026
2597
|
$ tiro notes transcript <guid> --format md --no-timestamps --output ./clean.md
|
|
2598
|
+
$ tiro wiki search "onboarding" --json
|
|
2599
|
+
$ tiro wiki graph <pageGuid> --mode around --radius 2
|
|
2027
2600
|
$ tiro mcp install # one-line setup for Claude Code
|
|
2028
2601
|
|
|
2029
2602
|
ENVIRONMENT
|
|
@@ -2036,11 +2609,12 @@ DOCS
|
|
|
2036
2609
|
https://api-docs.tiro.ooo/cli
|
|
2037
2610
|
`;
|
|
2038
2611
|
function buildProgram() {
|
|
2039
|
-
const program = new
|
|
2612
|
+
const program = new Command19();
|
|
2040
2613
|
program.name("tiro").description("Tiro AI notes & transcripts \u2014 agent-first command line").version(VERSION, "-v, --version", "Print version").option("--hostname <url>", "API base URL (default: https://api.tiro.ooo)").option("--json", "Force JSON output").option("--pretty", "Force pretty (human) output").option("--quiet", "Suppress non-error output").option("--verbose", "Verbose logging to stderr").option("--no-color", "Disable ANSI colors").addHelpText("after", EXAMPLES);
|
|
2041
2614
|
program.showHelpAfterError("(run `tiro --help` for available commands)");
|
|
2042
2615
|
registerAuth(program);
|
|
2043
2616
|
registerNotes(program);
|
|
2617
|
+
registerWiki(program);
|
|
2044
2618
|
registerMcp(program);
|
|
2045
2619
|
return program;
|
|
2046
2620
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theplato/tiro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Tiro AI notes & transcripts — agent-first command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"test:watch": "vitest",
|
|
25
|
-
"test:security": "vitest run --testNamePattern \"\\\\((C|H|M)[0-9]+\\\\)\"",
|
|
25
|
+
"test:security": "vitest run --testNamePattern \"\\\\((C|H|M|X)[0-9]+\\\\)\"",
|
|
26
26
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|