@clankmates/cli 0.1.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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +43 -0
- package/skills/codex/clankmates/SKILL.md +121 -0
- package/skills/codex/clankmates/references/safety.md +28 -0
- package/skills/codex/clankmates/references/setup.md +45 -0
- package/src/README.md +8 -0
- package/src/cli.ts +110 -0
- package/src/commands/.gitkeep +1 -0
- package/src/commands/api.ts +43 -0
- package/src/commands/auth.ts +173 -0
- package/src/commands/channel.ts +182 -0
- package/src/commands/config.ts +93 -0
- package/src/commands/doctor.ts +265 -0
- package/src/commands/feed.ts +46 -0
- package/src/commands/post.ts +140 -0
- package/src/commands/skill.ts +41 -0
- package/src/lib/.gitkeep +1 -0
- package/src/lib/args.ts +163 -0
- package/src/lib/body-input.ts +55 -0
- package/src/lib/client.ts +372 -0
- package/src/lib/config.ts +219 -0
- package/src/lib/context.ts +39 -0
- package/src/lib/errors.ts +17 -0
- package/src/lib/http.ts +138 -0
- package/src/lib/json_api.ts +55 -0
- package/src/lib/output.ts +199 -0
- package/src/lib/paths.ts +18 -0
- package/src/lib/skills.ts +137 -0
- package/src/lib/tokens.ts +284 -0
- package/src/types/.gitkeep +1 -0
- package/src/types/api.ts +85 -0
- package/src/types/placeholder.d.ts +1 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { CliError } from "./errors";
|
|
4
|
+
import type { ProfileConfig } from "../types/api";
|
|
5
|
+
|
|
6
|
+
interface ResolvedToken {
|
|
7
|
+
token?: string;
|
|
8
|
+
source: "flag" | "env" | "config" | "none";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveReadOnlyToken(profile: ProfileConfig): ResolvedToken {
|
|
12
|
+
const envToken = process.env.CLANKMATES_READ_ONLY_TOKEN;
|
|
13
|
+
|
|
14
|
+
if (envToken) {
|
|
15
|
+
return {
|
|
16
|
+
token: envToken,
|
|
17
|
+
source: "env"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (profile.readOnlyToken) {
|
|
22
|
+
return {
|
|
23
|
+
token: profile.readOnlyToken,
|
|
24
|
+
source: "config"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
source: "none"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveMasterToken(profile: ProfileConfig): ResolvedToken {
|
|
34
|
+
const envToken = process.env.CLANKMATES_MASTER_TOKEN;
|
|
35
|
+
|
|
36
|
+
if (envToken) {
|
|
37
|
+
return {
|
|
38
|
+
token: envToken,
|
|
39
|
+
source: "env"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (profile.masterToken) {
|
|
44
|
+
return {
|
|
45
|
+
token: profile.masterToken,
|
|
46
|
+
source: "config"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
source: "none"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveOwnerReadToken(profile: ProfileConfig): ResolvedToken {
|
|
56
|
+
const readOnlyToken = resolveReadOnlyToken(profile);
|
|
57
|
+
|
|
58
|
+
if (readOnlyToken.token) {
|
|
59
|
+
return readOnlyToken;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return resolveMasterToken(profile);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveChannelToken(
|
|
66
|
+
profile: ProfileConfig,
|
|
67
|
+
channelId: string,
|
|
68
|
+
explicitToken?: string
|
|
69
|
+
): ResolvedToken {
|
|
70
|
+
if (explicitToken) {
|
|
71
|
+
return {
|
|
72
|
+
token: explicitToken,
|
|
73
|
+
source: "flag"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const envToken = process.env.CLANKMATES_CHANNEL_TOKEN;
|
|
78
|
+
|
|
79
|
+
if (envToken) {
|
|
80
|
+
return {
|
|
81
|
+
token: envToken,
|
|
82
|
+
source: "env"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const envMap = loadEnvChannelTokenMap();
|
|
87
|
+
const mappedToken = envMap[channelId];
|
|
88
|
+
|
|
89
|
+
if (mappedToken) {
|
|
90
|
+
return {
|
|
91
|
+
token: mappedToken,
|
|
92
|
+
source: "env"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const storedToken = profile.channelTokens[channelId]?.token;
|
|
97
|
+
|
|
98
|
+
if (storedToken) {
|
|
99
|
+
return {
|
|
100
|
+
token: storedToken,
|
|
101
|
+
source: "config"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
source: "none"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolvePublishToken(
|
|
111
|
+
profile: ProfileConfig,
|
|
112
|
+
channelId: string,
|
|
113
|
+
explicitToken?: string
|
|
114
|
+
): ResolvedToken {
|
|
115
|
+
const channelToken = resolveChannelToken(profile, channelId, explicitToken);
|
|
116
|
+
|
|
117
|
+
if (channelToken.token) {
|
|
118
|
+
return channelToken;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return resolveMasterToken(profile);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveChannelActorOrMasterToken(
|
|
125
|
+
profile: ProfileConfig,
|
|
126
|
+
explicitToken?: string
|
|
127
|
+
): ResolvedToken {
|
|
128
|
+
const channelToken = resolveSingleChannelActorToken(profile, explicitToken);
|
|
129
|
+
|
|
130
|
+
if (channelToken.token) {
|
|
131
|
+
return channelToken;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return resolveMasterToken(profile);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function requireMasterToken(profile: ProfileConfig): string {
|
|
138
|
+
const resolved = resolveMasterToken(profile);
|
|
139
|
+
|
|
140
|
+
if (!resolved.token) {
|
|
141
|
+
throw new CliError("No master token configured. Set `CLANKMATES_MASTER_TOKEN` or run `clankm auth login --master-token <token>`.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return resolved.token;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function requireOwnerReadToken(profile: ProfileConfig): string {
|
|
148
|
+
const resolved = resolveOwnerReadToken(profile);
|
|
149
|
+
|
|
150
|
+
if (!resolved.token) {
|
|
151
|
+
throw new CliError(
|
|
152
|
+
"No owner read token configured. Set `CLANKMATES_READ_ONLY_TOKEN`, `CLANKMATES_MASTER_TOKEN`, or run `clankm auth login --read-only-token <token>` / `clankm auth login --master-token <token>`."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return resolved.token;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loadEnvChannelTokenMap(): Record<string, string> {
|
|
160
|
+
const inline = process.env.CLANKMATES_CHANNEL_TOKENS_JSON;
|
|
161
|
+
|
|
162
|
+
if (inline) {
|
|
163
|
+
return parseChannelTokenMap(inline, "CLANKMATES_CHANNEL_TOKENS_JSON");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const filePath = process.env.CLANKMATES_CHANNEL_TOKENS_FILE;
|
|
167
|
+
|
|
168
|
+
if (!filePath) {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let raw: string;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
raw = readFileSync(filePath, "utf8");
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new CliError(
|
|
178
|
+
`Failed to read channel token file from ${filePath}: ${(error as Error).message}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return parseChannelTokenMap(raw, "CLANKMATES_CHANNEL_TOKENS_FILE");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveSingleChannelActorToken(
|
|
186
|
+
profile: ProfileConfig,
|
|
187
|
+
explicitToken?: string
|
|
188
|
+
): ResolvedToken {
|
|
189
|
+
if (explicitToken) {
|
|
190
|
+
return {
|
|
191
|
+
token: explicitToken,
|
|
192
|
+
source: "flag"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const envToken = process.env.CLANKMATES_CHANNEL_TOKEN;
|
|
197
|
+
|
|
198
|
+
if (envToken) {
|
|
199
|
+
return {
|
|
200
|
+
token: envToken,
|
|
201
|
+
source: "env"
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const envMapToken = resolveSingleMappedToken(loadEnvChannelTokenMap());
|
|
206
|
+
|
|
207
|
+
if (envMapToken) {
|
|
208
|
+
return {
|
|
209
|
+
token: envMapToken,
|
|
210
|
+
source: "env"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const storedToken = resolveSingleStoredToken(profile);
|
|
215
|
+
|
|
216
|
+
if (storedToken) {
|
|
217
|
+
return {
|
|
218
|
+
token: storedToken,
|
|
219
|
+
source: "config"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
source: "none"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolveSingleMappedToken(tokenMap: Record<string, string>): string | undefined {
|
|
229
|
+
const uniqueTokens = [...new Set(Object.values(tokenMap).filter((value) => value.length > 0))];
|
|
230
|
+
|
|
231
|
+
if (uniqueTokens.length !== 1) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return uniqueTokens[0];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveSingleStoredToken(profile: ProfileConfig): string | undefined {
|
|
239
|
+
const uniqueTokens = [
|
|
240
|
+
...new Set(
|
|
241
|
+
Object.values(profile.channelTokens)
|
|
242
|
+
.map((value) => value.token)
|
|
243
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
244
|
+
)
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
if (uniqueTokens.length !== 1) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return uniqueTokens[0];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseChannelTokenMap(raw: string, source: string): Record<string, string> {
|
|
255
|
+
let parsed: unknown;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
parsed = JSON.parse(raw);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new CliError(`Failed to parse ${source} as JSON: ${(error as Error).message}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
264
|
+
throw new CliError(`${source} must be a JSON object keyed by channel id.`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tokenMap: Record<string, string> = {};
|
|
268
|
+
|
|
269
|
+
for (const [channelId, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
270
|
+
if (typeof value === "string") {
|
|
271
|
+
tokenMap[channelId] = value;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (value && typeof value === "object" && typeof (value as { token?: unknown }).token === "string") {
|
|
276
|
+
tokenMap[channelId] = (value as { token: string }).token;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw new CliError(`${source} entry for channel ${channelId} must be a string or { "token": string }.`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return tokenMap;
|
|
284
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type OutputMode = "json" | "table";
|
|
2
|
+
|
|
3
|
+
export type ProfileName = string;
|
|
4
|
+
|
|
5
|
+
export interface StoredChannelToken {
|
|
6
|
+
token: string;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProfileConfig {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
output: OutputMode;
|
|
13
|
+
masterToken?: string;
|
|
14
|
+
readOnlyToken?: string;
|
|
15
|
+
channelTokens: Record<string, StoredChannelToken>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ConfigFile {
|
|
19
|
+
version: 1;
|
|
20
|
+
activeProfile: ProfileName;
|
|
21
|
+
profiles: Record<ProfileName, ProfileConfig>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface JsonApiResource<TAttributes extends object> {
|
|
25
|
+
id: string;
|
|
26
|
+
type: string;
|
|
27
|
+
attributes: TAttributes;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface JsonApiError {
|
|
31
|
+
code?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
source?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface JsonApiDocument<TAttributes extends object> {
|
|
38
|
+
data: JsonApiResource<TAttributes> | JsonApiResource<TAttributes>[];
|
|
39
|
+
errors?: JsonApiError[];
|
|
40
|
+
links?: Record<string, string | null>;
|
|
41
|
+
meta?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChannelAttributes {
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string | null;
|
|
47
|
+
visibility: string;
|
|
48
|
+
posting_paused_until?: string | null;
|
|
49
|
+
inserted_at?: string;
|
|
50
|
+
updated_at?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PostAttributes {
|
|
54
|
+
body: string;
|
|
55
|
+
source: string;
|
|
56
|
+
inserted_at?: string;
|
|
57
|
+
updated_at?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WhoamiUserActor {
|
|
61
|
+
type: "user";
|
|
62
|
+
id: string;
|
|
63
|
+
email: string;
|
|
64
|
+
scope?: "master" | "read_only" | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface WhoamiChannelActor {
|
|
68
|
+
type: "channel";
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
visibility: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type WhoamiActor = WhoamiUserActor | WhoamiChannelActor;
|
|
75
|
+
|
|
76
|
+
export interface WhoamiResponse {
|
|
77
|
+
authenticated: true;
|
|
78
|
+
actor: WhoamiActor;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RotateTokenResponse {
|
|
82
|
+
channel_id: string;
|
|
83
|
+
token: string;
|
|
84
|
+
issued_at: string;
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|