@abraca/cli 1.9.1 → 2.3.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/dist/abracadabra-cli.cjs +2516 -575
- package/dist/abracadabra-cli.cjs.map +1 -1
- package/dist/abracadabra-cli.esm.js +2515 -576
- package/dist/abracadabra-cli.esm.js.map +1 -1
- package/package.json +10 -5
- package/src/commands/awareness.ts +11 -8
- package/src/commands/spaces.ts +19 -17
- package/src/commands/tree.ts +3 -3
- package/src/commands/wiki/connect.ts +69 -0
- package/src/commands/wiki/index.ts +471 -0
- package/src/commands/wiki/render.ts +91 -0
- package/src/commands/wiki/snapshot.ts +210 -0
- package/src/commands/wiki/types.ts +45 -0
- package/src/commands/wiki/wikipedia.ts +154 -0
- package/src/connection.ts +15 -50
- package/src/crypto.ts +5 -4
- package/src/index.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abraca/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "CLI for Abracadabra — interact with CRDT document workspaces from the terminal",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,12 +28,17 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@noble/ed25519": "^
|
|
32
|
-
"@noble/hashes": "^
|
|
31
|
+
"@noble/ed25519": "^3.1.0",
|
|
32
|
+
"@noble/hashes": "^2.2.0",
|
|
33
|
+
"wtf-plugin-api": "^2.0.1",
|
|
34
|
+
"wtf_wikipedia": "^10.4.1"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
|
-
"@abraca/dabra": ">=
|
|
37
|
+
"@abraca/dabra": ">=2.0.0",
|
|
36
38
|
"y-protocols": "^1.0.6",
|
|
37
39
|
"yjs": "^13.6.8"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@abraca/dabra": "2.3.0"
|
|
38
43
|
}
|
|
39
|
-
}
|
|
44
|
+
}
|
|
@@ -96,27 +96,30 @@ registerCommand({
|
|
|
96
96
|
registerCommand({
|
|
97
97
|
name: 'chat',
|
|
98
98
|
aliases: ['send'],
|
|
99
|
-
description: 'Send a chat message to a channel.',
|
|
100
|
-
usage: 'chat
|
|
99
|
+
description: 'Send a chat message to a channel doc.',
|
|
100
|
+
usage: 'chat channel_doc_id=<docId> text=<message>',
|
|
101
101
|
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
102
102
|
if (!conn) return 'Not connected'
|
|
103
103
|
|
|
104
104
|
const rootProvider = conn.rootProvider
|
|
105
105
|
if (!rootProvider) return 'Not connected'
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Accept either `channel_doc_id` or legacy `channel` (with optional
|
|
108
|
+
// `group:` prefix the new server doesn't use — strip it).
|
|
109
|
+
const raw = args.params['channel_doc_id'] || args.params['channel']
|
|
110
|
+
if (!raw) return 'Missing required parameter: channel_doc_id=<docId>'
|
|
111
|
+
const channel_doc_id = raw.startsWith('group:') ? raw.slice(6) : raw
|
|
109
112
|
|
|
110
113
|
const text = args.params['text'] || args.params['content'] || args.positional[0]
|
|
111
114
|
if (!text) return 'Missing required parameter: text=<message>'
|
|
112
115
|
|
|
113
116
|
rootProvider.sendStateless(JSON.stringify({
|
|
114
|
-
type: '
|
|
115
|
-
|
|
117
|
+
type: 'messages:send',
|
|
118
|
+
channel_doc_id,
|
|
116
119
|
content: text,
|
|
117
|
-
|
|
120
|
+
mentions: [],
|
|
118
121
|
}))
|
|
119
122
|
|
|
120
|
-
return `Sent to ${
|
|
123
|
+
return `Sent to ${channel_doc_id}`
|
|
121
124
|
},
|
|
122
125
|
})
|
package/src/commands/spaces.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { registerCommand } from '../command.ts'
|
|
5
5
|
import type { CLIConnection } from '../connection.ts'
|
|
6
6
|
import type { ParsedArgs } from '../parser.ts'
|
|
7
|
-
import { printJson, printTable, getFormat
|
|
8
|
-
import { readEntries
|
|
7
|
+
import { printJson, printTable, getFormat } from '../output.ts'
|
|
8
|
+
import { readEntries } from '../resolve.ts'
|
|
9
9
|
|
|
10
10
|
registerCommand({
|
|
11
11
|
name: 'info',
|
|
@@ -28,7 +28,7 @@ registerCommand({
|
|
|
28
28
|
`URL: ${conn.config.url}`,
|
|
29
29
|
`Version: ${si.version ?? '—'}`,
|
|
30
30
|
`Protocol: ${si.protocol_version ?? '—'}`,
|
|
31
|
-
`
|
|
31
|
+
`Active Doc: ${conn.rootDocId ?? '—'}`,
|
|
32
32
|
`Auth: ${si.auth_methods?.join(', ') ?? '—'}`,
|
|
33
33
|
`Registration:${si.registration_allowed ? ' open' : ' closed'}${si.invite_only ? ' (invite only)' : ''}`,
|
|
34
34
|
`Documents: ${docCount}`,
|
|
@@ -40,7 +40,7 @@ registerCommand({
|
|
|
40
40
|
|
|
41
41
|
registerCommand({
|
|
42
42
|
name: 'spaces',
|
|
43
|
-
description: 'List available spaces
|
|
43
|
+
description: 'List available spaces (top-level documents).',
|
|
44
44
|
usage: 'spaces [--format=json|tsv]',
|
|
45
45
|
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
46
46
|
if (!conn) return 'Not connected'
|
|
@@ -51,18 +51,20 @@ registerCommand({
|
|
|
51
51
|
const active = conn.rootDocId
|
|
52
52
|
|
|
53
53
|
if (format === 'json') {
|
|
54
|
-
return printJson(spaces.map(s => ({ ...s, active: s.
|
|
54
|
+
return printJson(spaces.map(s => ({ ...s, active: s.id === active })))
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const rows = spaces.map(s =>
|
|
58
|
-
s.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const rows = spaces.map(s => {
|
|
58
|
+
const visibility = s.public_access === 'observer' ? 'public' : 'private'
|
|
59
|
+
return [
|
|
60
|
+
s.id === active ? '▸' : ' ',
|
|
61
|
+
s.id.slice(0, 8) + '…',
|
|
62
|
+
s.label ?? s.id,
|
|
63
|
+
visibility,
|
|
64
|
+
]
|
|
65
|
+
})
|
|
64
66
|
|
|
65
|
-
return printTable(rows, ['', 'ID', 'NAME', '
|
|
67
|
+
return printTable(rows, ['', 'ID', 'NAME', 'VISIBILITY'])
|
|
66
68
|
},
|
|
67
69
|
})
|
|
68
70
|
|
|
@@ -84,9 +86,9 @@ registerCommand({
|
|
|
84
86
|
} else if (targetName) {
|
|
85
87
|
const lower = targetName.toLowerCase()
|
|
86
88
|
const space = conn.spaces.find(s =>
|
|
87
|
-
s.
|
|
89
|
+
(s.label ?? s.id).toLowerCase() === lower || s.id === targetName
|
|
88
90
|
)
|
|
89
|
-
if (space) docId = space.
|
|
91
|
+
if (space) docId = space.id
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
if (!docId) {
|
|
@@ -94,7 +96,7 @@ registerCommand({
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
await conn.switchSpace(docId)
|
|
97
|
-
const space = conn.spaces.find(s => s.
|
|
98
|
-
return `Switched to space "${space?.
|
|
99
|
+
const space = conn.spaces.find(s => s.id === docId)
|
|
100
|
+
return `Switched to space "${space?.label ?? docId}"`
|
|
99
101
|
},
|
|
100
102
|
})
|
package/src/commands/tree.ts
CHANGED
|
@@ -51,9 +51,9 @@ registerCommand({
|
|
|
51
51
|
return '(empty tree)'
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Find root label for header
|
|
55
|
-
const
|
|
56
|
-
const rootLabel =
|
|
54
|
+
// Find root label for header — the active Space, falling back to its id.
|
|
55
|
+
const activeSpace = conn.spaces.find(s => s.id === conn.rootDocId)
|
|
56
|
+
const rootLabel = activeSpace?.label ?? conn.rootDocId ?? 'Workspace'
|
|
57
57
|
|
|
58
58
|
return rootLabel + '\n' + printTree(toTreeNodes(tree))
|
|
59
59
|
},
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a DocumentManager session for the wiki command, mirroring the
|
|
3
|
+
* auth/register flow that CLIConnection uses but using the modern public API.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the CLI's Ed25519 keypair handling (loadOrCreateKeypair, signChallenge)
|
|
6
|
+
* so the wiki command authenticates with the same identity as every other
|
|
7
|
+
* subcommand.
|
|
8
|
+
*/
|
|
9
|
+
import { DocumentManager } from '@abraca/dabra'
|
|
10
|
+
import { loadOrCreateKeypair, signChallenge } from '../../crypto.ts'
|
|
11
|
+
|
|
12
|
+
export interface OpenSessionConfig {
|
|
13
|
+
url: string
|
|
14
|
+
name?: string
|
|
15
|
+
color?: string
|
|
16
|
+
inviteCode?: string
|
|
17
|
+
keyFile?: string
|
|
18
|
+
/** Suppress informational stderr logging. */
|
|
19
|
+
quiet?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OpenSessionResult {
|
|
23
|
+
dm: DocumentManager
|
|
24
|
+
/** Active root doc id (the entry-point space). */
|
|
25
|
+
rootDocId: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function openSession(config: OpenSessionConfig): Promise<OpenSessionResult> {
|
|
29
|
+
const keypair = await loadOrCreateKeypair(config.keyFile)
|
|
30
|
+
const sign = (challenge: string) => Promise.resolve(signChallenge(challenge, keypair.privateKey))
|
|
31
|
+
|
|
32
|
+
const dm = new DocumentManager({
|
|
33
|
+
url: config.url,
|
|
34
|
+
name: config.name ?? 'Wiki Extractor',
|
|
35
|
+
color: config.color,
|
|
36
|
+
quiet: config.quiet,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Authenticate first; register on first run.
|
|
40
|
+
try {
|
|
41
|
+
await dm.client.loginWithKey(keypair.publicKeyB64, sign)
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
const status = err?.status ?? err?.response?.status
|
|
44
|
+
if (status === 404 || status === 422) {
|
|
45
|
+
if (!config.quiet) {
|
|
46
|
+
console.error('[abracadabra] Key not registered, creating new account...')
|
|
47
|
+
}
|
|
48
|
+
await dm.client.registerWithKey({
|
|
49
|
+
publicKey: keypair.publicKeyB64,
|
|
50
|
+
username: (config.name ?? 'wiki-extractor').replace(/\s+/g, '-').toLowerCase(),
|
|
51
|
+
displayName: config.name ?? 'Wiki Extractor',
|
|
52
|
+
deviceName: 'CLI Wiki',
|
|
53
|
+
inviteCode: config.inviteCode,
|
|
54
|
+
})
|
|
55
|
+
await dm.client.loginWithKey(keypair.publicKeyB64, sign)
|
|
56
|
+
} else {
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await dm.connect()
|
|
62
|
+
|
|
63
|
+
const rootDocId = dm.rootDocId
|
|
64
|
+
if (!rootDocId) {
|
|
65
|
+
throw new Error('Connected but no rootDocId — server has no spaces.')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { dm, rootDocId }
|
|
69
|
+
}
|