@clankid/cli 0.17.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/bin/clank +6 -0
  5. package/docs/usage/clankid-migration.md +59 -0
  6. package/package.json +47 -0
  7. package/skills/codex/clankid/SKILL.md +256 -0
  8. package/skills/codex/clankid/references/safety.md +28 -0
  9. package/skills/codex/clankid/references/setup.md +65 -0
  10. package/src/README.md +8 -0
  11. package/src/cli.ts +154 -0
  12. package/src/commands/.gitkeep +1 -0
  13. package/src/commands/account.ts +337 -0
  14. package/src/commands/api.ts +43 -0
  15. package/src/commands/auth/access-keys.ts +206 -0
  16. package/src/commands/auth.ts +240 -0
  17. package/src/commands/cache.ts +124 -0
  18. package/src/commands/config.ts +93 -0
  19. package/src/commands/doctor/checks.ts +123 -0
  20. package/src/commands/doctor/render.ts +140 -0
  21. package/src/commands/doctor/suggestions.ts +42 -0
  22. package/src/commands/doctor/types.ts +75 -0
  23. package/src/commands/doctor.ts +221 -0
  24. package/src/commands/feed.ts +185 -0
  25. package/src/commands/identity/keys.ts +145 -0
  26. package/src/commands/identity/render.ts +224 -0
  27. package/src/commands/identity/validation.ts +11 -0
  28. package/src/commands/identity.ts +295 -0
  29. package/src/commands/inbox/content.ts +13 -0
  30. package/src/commands/inbox/filters.ts +70 -0
  31. package/src/commands/inbox/messages.ts +71 -0
  32. package/src/commands/inbox/participants.ts +152 -0
  33. package/src/commands/inbox/render.ts +13 -0
  34. package/src/commands/inbox/resource-output.ts +217 -0
  35. package/src/commands/inbox/schema.ts +185 -0
  36. package/src/commands/inbox/screening.ts +76 -0
  37. package/src/commands/inbox/sync-scopes.ts +62 -0
  38. package/src/commands/inbox/thread-output.ts +345 -0
  39. package/src/commands/inbox/watch.ts +207 -0
  40. package/src/commands/inbox.ts +374 -0
  41. package/src/commands/post.ts +406 -0
  42. package/src/commands/setup.ts +228 -0
  43. package/src/commands/skill.ts +41 -0
  44. package/src/commands/user.ts +33 -0
  45. package/src/lib/.gitkeep +1 -0
  46. package/src/lib/args.ts +231 -0
  47. package/src/lib/body-input.ts +55 -0
  48. package/src/lib/cache/scopes.ts +272 -0
  49. package/src/lib/cache/store.ts +195 -0
  50. package/src/lib/cache/types.ts +31 -0
  51. package/src/lib/cache.ts +135 -0
  52. package/src/lib/client/auth.ts +163 -0
  53. package/src/lib/client/core.ts +133 -0
  54. package/src/lib/client/feed.ts +93 -0
  55. package/src/lib/client/identities.ts +364 -0
  56. package/src/lib/client/identity-keys.ts +57 -0
  57. package/src/lib/client/inbox.ts +385 -0
  58. package/src/lib/client/posts.ts +236 -0
  59. package/src/lib/client/raw-api.ts +33 -0
  60. package/src/lib/client/users.ts +88 -0
  61. package/src/lib/client.ts +469 -0
  62. package/src/lib/config.ts +239 -0
  63. package/src/lib/content.ts +149 -0
  64. package/src/lib/context.ts +43 -0
  65. package/src/lib/errors.ts +17 -0
  66. package/src/lib/help.ts +1587 -0
  67. package/src/lib/http.ts +138 -0
  68. package/src/lib/human.ts +143 -0
  69. package/src/lib/json-input.ts +73 -0
  70. package/src/lib/json_api.ts +120 -0
  71. package/src/lib/output.ts +203 -0
  72. package/src/lib/pagination.ts +116 -0
  73. package/src/lib/paths.ts +44 -0
  74. package/src/lib/polling.ts +146 -0
  75. package/src/lib/post-output.ts +60 -0
  76. package/src/lib/skills.ts +137 -0
  77. package/src/lib/tokens.ts +284 -0
  78. package/src/lib/version.ts +19 -0
  79. package/src/types/.gitkeep +1 -0
  80. package/src/types/api.ts +320 -0
  81. package/src/types/placeholder.d.ts +1 -0
@@ -0,0 +1,116 @@
1
+ import type { ParsedArgs } from "./args";
2
+
3
+ const CLI_NAME = "clank";
4
+
5
+ const PAGINATION_FLAG_ORDER: Array<
6
+ [key: string, flag: string]
7
+ > = [
8
+ ["profile", "--profile"],
9
+ ["baseUrl", "--base-url"],
10
+ ["identity", "--identity"],
11
+ ["identityId", "--identity"],
12
+ ["status", "--status"],
13
+ ["mailbox", "--mailbox"],
14
+ ["participant", "--participant"],
15
+ ["participantScope", "--participant-scope"],
16
+ ["order", "--order"],
17
+ ["since", "--since"],
18
+ ["before", "--before"],
19
+ ["query", "--query"],
20
+ ["limit", "--limit"],
21
+ ];
22
+
23
+ const PAGINATION_BOOLEAN_FLAGS: Array<[key: string, flag: string]> = [
24
+ ["sinceCache", "--since-cache"],
25
+ ["saveCache", "--save-cache"],
26
+ ["hasAttachment", "--has-attachment"],
27
+ ];
28
+
29
+ export interface PaginationInfo {
30
+ nextCursor: string;
31
+ nextCommand?: string;
32
+ }
33
+
34
+ export function paginationInfo(
35
+ args: ParsedArgs,
36
+ nextCursor?: string | null,
37
+ options: { json?: boolean } = {},
38
+ ): PaginationInfo | undefined {
39
+ if (!nextCursor) {
40
+ return undefined;
41
+ }
42
+
43
+ return {
44
+ nextCursor,
45
+ nextCommand: args.flags.identityKey
46
+ ? undefined
47
+ : nextCommand(args, nextCursor, options),
48
+ };
49
+ }
50
+
51
+ export function paginatedJson<T extends object>(
52
+ args: ParsedArgs,
53
+ response: T & { nextCursor?: string },
54
+ ): T & { nextCursor?: string; pagination?: PaginationInfo } {
55
+ return {
56
+ ...response,
57
+ pagination: paginationInfo(args, response.nextCursor, { json: true }),
58
+ };
59
+ }
60
+
61
+ function paginationFlagParts(args: ParsedArgs): string[] {
62
+ const parts: string[] = [];
63
+ const seenFlags = new Set<string>();
64
+
65
+ for (const [key, flag] of PAGINATION_FLAG_ORDER) {
66
+ if (seenFlags.has(flag)) {
67
+ continue;
68
+ }
69
+
70
+ const value = args.flags[key];
71
+
72
+ if (typeof value !== "string") {
73
+ continue;
74
+ }
75
+
76
+ parts.push(flag, value);
77
+ seenFlags.add(flag);
78
+ }
79
+
80
+ for (const [key, flag] of PAGINATION_BOOLEAN_FLAGS) {
81
+ if (args.flags[key] === true) {
82
+ parts.push(flag);
83
+ }
84
+ }
85
+
86
+ return parts;
87
+ }
88
+
89
+ function nextCommand(
90
+ args: ParsedArgs,
91
+ nextCursor: string,
92
+ options: { json?: boolean },
93
+ ): string {
94
+ const parts = [
95
+ CLI_NAME,
96
+ ...args.commandPath,
97
+ ...args.positionals,
98
+ ...paginationFlagParts(args),
99
+ "--cursor",
100
+ nextCursor,
101
+ ];
102
+
103
+ if (options.json) {
104
+ parts.push("--json");
105
+ }
106
+
107
+ return parts.map(shellQuote).join(" ");
108
+ }
109
+
110
+ function shellQuote(value: string): string {
111
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
112
+ return value;
113
+ }
114
+
115
+ return `'${value.replace(/'/g, "'\\''")}'`;
116
+ }
@@ -0,0 +1,44 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export function getConfigPath(): string {
5
+ const explicit = process.env.CLANKID_CONFIG_PATH;
6
+
7
+ if (explicit) {
8
+ return explicit;
9
+ }
10
+
11
+ const xdg = process.env.XDG_CONFIG_HOME;
12
+
13
+ if (xdg) {
14
+ return path.join(xdg, "clankid", "config.json");
15
+ }
16
+
17
+ return path.join(os.homedir(), ".config", "clankid", "config.json");
18
+ }
19
+
20
+ export function getCachePath(): string {
21
+ const explicit = process.env.CLANKID_CACHE_PATH;
22
+
23
+ if (explicit) {
24
+ return explicit;
25
+ }
26
+
27
+ const xdg = process.env.XDG_CACHE_HOME;
28
+
29
+ if (xdg) {
30
+ return path.join(xdg, "clankid", "clank-cache.sqlite");
31
+ }
32
+
33
+ if (process.platform === "darwin") {
34
+ return path.join(
35
+ os.homedir(),
36
+ "Library",
37
+ "Caches",
38
+ "clankid",
39
+ "clank-cache.sqlite",
40
+ );
41
+ }
42
+
43
+ return path.join(os.homedir(), ".cache", "clankid", "clank-cache.sqlite");
44
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ cacheFlags,
3
+ prepareCachePlan,
4
+ saveCacheTimestamp,
5
+ type CachePlan,
6
+ type CacheResult,
7
+ type CacheScope,
8
+ } from "./cache";
9
+ import { stringFlag, type ParsedArgs } from "./args";
10
+ import type { CommandContext } from "./context";
11
+ import { CliError } from "./errors";
12
+ import { formatTimestamp, renderFields } from "./human";
13
+ import { printValue, type Io } from "./output";
14
+ import type { ChangeCheckResponse, LatestFirstOrder } from "../types/api";
15
+
16
+ export function parseLatestFirstOrder(
17
+ value: string | undefined,
18
+ ): LatestFirstOrder | undefined {
19
+ if (!value) {
20
+ return undefined;
21
+ }
22
+
23
+ if (value === "latest" || value === "oldest") {
24
+ return value;
25
+ }
26
+
27
+ throw new CliError("--order must be one of: latest, oldest", 2);
28
+ }
29
+
30
+ export function requiredSince(
31
+ args: ParsedArgs,
32
+ cachePlan?: CachePlan,
33
+ label = "resource",
34
+ ): string {
35
+ const since = stringFlag(args.flags, "since");
36
+
37
+ if (since) {
38
+ return since;
39
+ }
40
+
41
+ if (cachePlan?.previousServerTimestamp) {
42
+ return cachePlan.previousServerTimestamp;
43
+ }
44
+
45
+ if (cacheFlags(args).sinceCache) {
46
+ throw new CliError(
47
+ `No cached server timestamp for this ${label} scope. Run a read command with \`--save-cache\` first.`,
48
+ 2,
49
+ );
50
+ }
51
+
52
+ throw new CliError("Missing `--since`", 2);
53
+ }
54
+
55
+ export function resolvedSince(
56
+ args: ParsedArgs,
57
+ cachePlan: CachePlan | undefined,
58
+ ): string | undefined {
59
+ return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
60
+ }
61
+
62
+ export async function maybePrepareCachePlan(
63
+ args: ParsedArgs,
64
+ context: CommandContext,
65
+ scope: CacheScope | undefined,
66
+ ): Promise<CachePlan | undefined> {
67
+ return cacheFlags(args).sinceCache && scope
68
+ ? prepareCachePlan(context, scope)
69
+ : undefined;
70
+ }
71
+
72
+ export async function maybeSaveCacheTimestamp(
73
+ args: ParsedArgs,
74
+ context: CommandContext,
75
+ scope: CacheScope | undefined,
76
+ meta: Record<string, unknown> | undefined,
77
+ shouldSave: boolean,
78
+ ): Promise<string | undefined> {
79
+ return cacheFlags(args).saveCache && scope && shouldSave
80
+ ? saveCacheTimestamp(context, scope, meta)
81
+ : undefined;
82
+ }
83
+
84
+ export function renderCacheNote(cache: CacheResult | undefined): string | undefined {
85
+ if (!cache) {
86
+ return undefined;
87
+ }
88
+
89
+ const details = [
90
+ cache.previousServerTimestamp
91
+ ? `used ${cache.previousServerTimestamp}`
92
+ : cache.hit
93
+ ? "used cache"
94
+ : "no cached timestamp",
95
+ cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
96
+ ].filter(Boolean);
97
+
98
+ return `Cache: ${details.join("; ")}.`;
99
+ }
100
+
101
+ export function printCacheNote(io: Io, cache: CacheResult | undefined): void {
102
+ const note = renderCacheNote(cache);
103
+
104
+ if (note) {
105
+ io.stdout(note);
106
+ }
107
+ }
108
+
109
+ export function cacheFields(
110
+ cache: CacheResult | undefined,
111
+ ): Array<[string, string | undefined]> {
112
+ if (!cache) {
113
+ return [];
114
+ }
115
+
116
+ return [
117
+ ["Cache scope", cache.scopeKey],
118
+ ["Cached timestamp", cache.previousServerTimestamp],
119
+ ["Saved timestamp", cache.savedServerTimestamp],
120
+ ];
121
+ }
122
+
123
+ export function printChangeCheckResponse(
124
+ context: CommandContext,
125
+ io: Io,
126
+ response: ChangeCheckResponse,
127
+ cache?: CacheResult,
128
+ ): void {
129
+ printValue(
130
+ io,
131
+ context.outputMode,
132
+ context.outputMode === "json"
133
+ ? { ...response, ...(cache ? { cache } : {}) }
134
+ : renderFields([
135
+ ["Has updates", response.has_updates ? "yes" : "no"],
136
+ ["Server time", formatTimestamp(response.server_time)],
137
+ [
138
+ "Recommended poll",
139
+ response.recommended_poll_after_ms === undefined
140
+ ? undefined
141
+ : `${response.recommended_poll_after_ms}ms`,
142
+ ],
143
+ ...cacheFields(cache),
144
+ ]),
145
+ );
146
+ }
@@ -0,0 +1,60 @@
1
+ import type { CacheResult } from "./cache";
2
+ import { renderPagination } from "./human";
3
+ import { printJson, printValue, type Io } from "./output";
4
+ import { paginatedJson, paginationInfo } from "./pagination";
5
+ import { printCacheNote } from "./polling";
6
+ import type { ParsedArgs } from "./args";
7
+ import type { OutputMode, PostAttributes } from "../types/api";
8
+
9
+ export function printPostCollection(
10
+ args: ParsedArgs,
11
+ outputMode: OutputMode,
12
+ io: Io,
13
+ response: {
14
+ items: Array<{ id: string; attributes: PostAttributes }>;
15
+ nextCursor?: string;
16
+ meta?: Record<string, unknown>;
17
+ },
18
+ cache?: CacheResult,
19
+ ): void {
20
+ if (outputMode === "json") {
21
+ printJson(
22
+ io,
23
+ paginatedJson(args, {
24
+ items: response.items,
25
+ nextCursor: response.nextCursor,
26
+ meta: response.meta,
27
+ ...(cache ? { cache } : {}),
28
+ }),
29
+ );
30
+ return;
31
+ }
32
+
33
+ printValue(
34
+ io,
35
+ outputMode,
36
+ response.items.map((item) => ({
37
+ id: item.id,
38
+ source: item.attributes.source,
39
+ content: item.attributes.content_format ?? "markdown",
40
+ date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
41
+ body: item.attributes.body || payloadSummary(item.attributes.payload),
42
+ })),
43
+ );
44
+
45
+ const pagination = paginationInfo(args, response.nextCursor);
46
+ const message = renderPagination(
47
+ pagination?.nextCursor,
48
+ pagination?.nextCommand,
49
+ );
50
+
51
+ if (message) {
52
+ io.stdout(message);
53
+ }
54
+
55
+ printCacheNote(io, cache);
56
+ }
57
+
58
+ function payloadSummary(payload?: Record<string, unknown> | null): string {
59
+ return payload ? JSON.stringify(payload) : "";
60
+ }
@@ -0,0 +1,137 @@
1
+ import { cp, lstat, mkdir, readlink, rm, symlink } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { CliError } from "./errors";
7
+
8
+ export type SkillHost = "codex" | "claude" | "both";
9
+ export type SkillInstallMode = "symlink" | "copy";
10
+
11
+ export interface InstalledSkillTarget {
12
+ host: Exclude<SkillHost, "both">;
13
+ mode: SkillInstallMode;
14
+ sourcePath: string;
15
+ targetPath: string;
16
+ status: "installed" | "updated" | "already_installed";
17
+ }
18
+
19
+ const SKILL_NAME = "clankid";
20
+
21
+ export function bundledSkillPath(): string {
22
+ return fileURLToPath(
23
+ new URL("../../skills/codex/clankid", import.meta.url),
24
+ );
25
+ }
26
+
27
+ export function resolveSkillHosts(host: string | undefined): Exclude<SkillHost, "both">[] {
28
+ switch (host ?? "both") {
29
+ case "codex":
30
+ return ["codex"];
31
+ case "claude":
32
+ case "claude-code":
33
+ return ["claude"];
34
+ case "both":
35
+ return ["codex", "claude"];
36
+ default:
37
+ throw new CliError(
38
+ "Unsupported `--host`. Use `codex`, `claude`, or `both`.",
39
+ 2,
40
+ );
41
+ }
42
+ }
43
+
44
+ export function resolveSkillInstallTarget(host: Exclude<SkillHost, "both">): string {
45
+ if (host === "codex") {
46
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
47
+ return path.join(codexHome, "skills", SKILL_NAME);
48
+ }
49
+
50
+ const claudeHome = process.env.CLAUDE_HOME || path.join(os.homedir(), ".claude");
51
+ return path.join(claudeHome, "skills", SKILL_NAME);
52
+ }
53
+
54
+ export async function installBundledSkill(input: {
55
+ host: Exclude<SkillHost, "both">;
56
+ mode: SkillInstallMode;
57
+ force: boolean;
58
+ }): Promise<InstalledSkillTarget> {
59
+ const sourcePath = bundledSkillPath();
60
+ const targetPath = resolveSkillInstallTarget(input.host);
61
+ const existing = await readInstallState(targetPath);
62
+
63
+ if (existing.exists) {
64
+ if (input.mode === "symlink" && existing.kind === "symlink" && existing.linkTarget === sourcePath) {
65
+ return {
66
+ host: input.host,
67
+ mode: input.mode,
68
+ sourcePath,
69
+ targetPath,
70
+ status: "already_installed",
71
+ };
72
+ }
73
+
74
+ if (!input.force) {
75
+ throw new CliError(
76
+ `Skill target already exists at ${targetPath}. Re-run with \`--force\` to replace it.`,
77
+ 2,
78
+ );
79
+ }
80
+
81
+ await rm(targetPath, { recursive: true, force: true });
82
+ }
83
+
84
+ await mkdir(path.dirname(targetPath), { recursive: true });
85
+
86
+ if (input.mode === "copy") {
87
+ await cp(sourcePath, targetPath, { recursive: true });
88
+ } else {
89
+ await symlink(sourcePath, targetPath, "dir");
90
+ }
91
+
92
+ return {
93
+ host: input.host,
94
+ mode: input.mode,
95
+ sourcePath,
96
+ targetPath,
97
+ status: existing.exists ? "updated" : "installed",
98
+ };
99
+ }
100
+
101
+ async function readInstallState(targetPath: string): Promise<{
102
+ exists: boolean;
103
+ kind?: "directory" | "symlink" | "file";
104
+ linkTarget?: string;
105
+ }> {
106
+ try {
107
+ const stats = await lstat(targetPath);
108
+
109
+ if (stats.isSymbolicLink()) {
110
+ return {
111
+ exists: true,
112
+ kind: "symlink",
113
+ linkTarget: await readlink(targetPath),
114
+ };
115
+ }
116
+
117
+ if (stats.isDirectory()) {
118
+ return {
119
+ exists: true,
120
+ kind: "directory",
121
+ };
122
+ }
123
+
124
+ return {
125
+ exists: true,
126
+ kind: "file",
127
+ };
128
+ } catch (error) {
129
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
130
+ return { exists: false };
131
+ }
132
+
133
+ throw new CliError(
134
+ `Failed to inspect skill target ${targetPath}: ${(error as Error).message}`,
135
+ );
136
+ }
137
+ }