ente-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.
@@ -0,0 +1,345 @@
1
+ import { resolveEntity, resolveEntities, resolveSpaceByName } from "../resolve.js";
2
+ import { formatActivityList, formatActivityDetail, formatTaskList, formatSpacesFooter, jsonOutputWithSpaces, } from "../format.js";
3
+ import { parseAttrs } from "../utils.js";
4
+ /**
5
+ * Shared activity-list logic used by both `activity list` and `ente tasks`.
6
+ */
7
+ const VALID_SORT_FIELDS = ["deadline", "created"];
8
+ export async function runActivityList(client, opts) {
9
+ if (opts.sort && !VALID_SORT_FIELDS.includes(opts.sort)) {
10
+ throw new Error(`Invalid --sort value '${opts.sort}'. Allowed values: ${VALID_SORT_FIELDS.join(", ")}`);
11
+ }
12
+ let entityId;
13
+ if (opts.entity) {
14
+ entityId = await resolveEntity(client, opts.entity);
15
+ }
16
+ const spaces = await client.listSpaces();
17
+ // When --type task + --only-open, use the richer listTasks API
18
+ if (opts.type === "task" && opts.onlyOpen) {
19
+ if (opts.before !== undefined || opts.after !== undefined) {
20
+ throw new Error("--before/--after cannot be used with --type task --only-open. Remove --only-open to use --before/--after.");
21
+ }
22
+ const res = await client.listTasks({ entityId, onlyOpen: true });
23
+ let tasks = res.data;
24
+ // Apply --hours filter
25
+ if (opts.hours) {
26
+ const cutoff = Date.now() - opts.hours * 60 * 60 * 1000;
27
+ tasks = tasks.filter((t) => t.activity.createdAt >= cutoff);
28
+ }
29
+ // Apply --attr filter
30
+ const attrFilter = parseAttrs(opts.attr);
31
+ if (Object.keys(attrFilter).length > 0) {
32
+ tasks = tasks.filter((t) => {
33
+ const attrs = t.activity.attributes ?? {};
34
+ return Object.entries(attrFilter).every(([k, v]) => String(attrs[k] ?? "") === v);
35
+ });
36
+ }
37
+ // Sort by deadline (default for tasks) or created
38
+ if (opts.sort === "created") {
39
+ tasks.sort((a, b) => b.activity.createdAt - a.activity.createdAt);
40
+ }
41
+ else {
42
+ // default: deadline ascending
43
+ tasks.sort((a, b) => {
44
+ const da = a.activity.deadline ?? Infinity;
45
+ const db = b.activity.deadline ?? Infinity;
46
+ return da - db;
47
+ });
48
+ }
49
+ // Apply --limit after sorting
50
+ if (opts.limit) {
51
+ tasks = tasks.slice(0, opts.limit);
52
+ }
53
+ if (opts.json) {
54
+ console.log(jsonOutputWithSpaces({ data: tasks, users: res.users }, spaces));
55
+ }
56
+ else {
57
+ console.log(formatTaskList(tasks, res.users));
58
+ console.log(formatSpacesFooter(spaces));
59
+ }
60
+ return;
61
+ }
62
+ // General activity list path
63
+ // Note: --sort is excluded from hasClientFilters because sorting can be
64
+ // applied to whatever page the server returns. Including it here would
65
+ // trigger full pagination (up to 10K items) just to re-order results.
66
+ const hasClientFilters = opts.type || opts.onlyOpen || (opts.attr && opts.attr.length > 0);
67
+ let activities;
68
+ let serverHasMore;
69
+ if (hasClientFilters) {
70
+ // Paginate through all server pages to avoid silent data truncation
71
+ // (server caps at 200 per page)
72
+ const MAX_PAGINATED_ITEMS = 10_000;
73
+ const MAX_PAGES = 50;
74
+ const hoursCutoff = opts.hours
75
+ ? Date.now() - opts.hours * 60 * 60 * 1000
76
+ : undefined;
77
+ activities = [];
78
+ let before = opts.before;
79
+ let pages = 0;
80
+ let hitCap = false;
81
+ for (;;) {
82
+ const page = await client.listActivities({
83
+ entityId,
84
+ limit: 200,
85
+ before,
86
+ after: opts.after,
87
+ });
88
+ let pageData = page.data;
89
+ // Apply --hours as a client-side filter since server may not support it
90
+ if (hoursCutoff) {
91
+ pageData = pageData.filter((a) => a.createdAt >= hoursCutoff);
92
+ // If any items were filtered out, all subsequent pages will be older
93
+ if (pageData.length < page.data.length) {
94
+ activities.push(...pageData);
95
+ break;
96
+ }
97
+ }
98
+ activities.push(...pageData);
99
+ pages++;
100
+ if (!page.hasMore || page.data.length === 0)
101
+ break;
102
+ // Safety caps to prevent unbounded pagination
103
+ if (activities.length >= MAX_PAGINATED_ITEMS || pages >= MAX_PAGES) {
104
+ hitCap = true;
105
+ break;
106
+ }
107
+ // Use the oldest item's createdAt as the cursor for the next page
108
+ before = page.data[page.data.length - 1].createdAt;
109
+ }
110
+ if (hitCap) {
111
+ console.error(`Warning: pagination cap reached (${activities.length} items / ${pages} pages). Results may be incomplete — narrow your query with --hours, --before, or --entity.`);
112
+ }
113
+ }
114
+ else {
115
+ const res = await client.listActivities({
116
+ entityId,
117
+ hoursBack: opts.hours,
118
+ limit: opts.limit,
119
+ before: opts.before,
120
+ after: opts.after,
121
+ });
122
+ activities = res.data;
123
+ serverHasMore = res.hasMore;
124
+ }
125
+ // Client-side --type filter
126
+ if (opts.type) {
127
+ activities = activities.filter((a) => a.type === opts.type);
128
+ }
129
+ // Client-side --only-open filter
130
+ if (opts.onlyOpen) {
131
+ activities = activities.filter((a) => !a.completed);
132
+ }
133
+ // Client-side --attr filter
134
+ const attrFilter = parseAttrs(opts.attr);
135
+ if (Object.keys(attrFilter).length > 0) {
136
+ activities = activities.filter((a) => {
137
+ const attrs = a.attributes ?? {};
138
+ return Object.entries(attrFilter).every(([k, v]) => String(attrs[k] ?? "") === v);
139
+ });
140
+ }
141
+ // Sort before limiting to get correct top-N results
142
+ if (opts.sort === "deadline") {
143
+ activities.sort((a, b) => {
144
+ const da = a.deadline ?? Infinity;
145
+ const db = b.deadline ?? Infinity;
146
+ return da - db;
147
+ });
148
+ }
149
+ else if (opts.sort === "created") {
150
+ activities.sort((a, b) => b.createdAt - a.createdAt);
151
+ }
152
+ // Apply --limit post-filter and post-sort
153
+ if (hasClientFilters && opts.limit) {
154
+ activities = activities.slice(0, opts.limit);
155
+ }
156
+ const hasMore = hasClientFilters ? undefined : serverHasMore;
157
+ if (opts.json) {
158
+ console.log(jsonOutputWithSpaces({ data: activities, ...(hasMore !== undefined && { hasMore }) }, spaces));
159
+ }
160
+ else {
161
+ console.log(formatActivityList(activities));
162
+ if (hasMore && activities.length > 0) {
163
+ const oldest = activities[activities.length - 1];
164
+ console.log(`\n(more results available — use --before ${oldest.createdAt})`);
165
+ }
166
+ console.log(formatSpacesFooter(spaces));
167
+ }
168
+ }
169
+ export function registerActivityCommands(program, getClient) {
170
+ const activity = program
171
+ .command("activity")
172
+ .description("Manage activities");
173
+ // -- create ---------------------------------------------------------------
174
+ activity
175
+ .command("create")
176
+ .description("Create an activity (note, conversation, call, message)")
177
+ .requiredOption("--type <type>", "Activity type (note, conversation, call, message)")
178
+ .requiredOption("--title <title>", "Activity title")
179
+ .requiredOption("--entity <names...>", "Linked entities (resolved by name)")
180
+ .requiredOption("--space <name-or-id>", "Space to create the activity in (name or ID). Determines who can see this activity.")
181
+ .option("--content <text>", "Activity content")
182
+ .option("--auto-link", "Auto-link entities not yet in the target space (only works for personal space). Use when referencing business entities in a private context.")
183
+ .option("--json", "Output raw JSON")
184
+ .action(async (opts) => {
185
+ const client = getClient();
186
+ const entityIds = await resolveEntities(client, opts.entity);
187
+ const spaceId = await resolveSpaceByName(client, opts.space);
188
+ const res = await client.createActivity({
189
+ entityIds,
190
+ type: opts.type,
191
+ title: opts.title,
192
+ content: opts.content,
193
+ spaceId,
194
+ autoLinkPersonalSpace: opts.autoLink ?? false,
195
+ });
196
+ const spaces = await client.listSpaces();
197
+ if (opts.json) {
198
+ console.log(jsonOutputWithSpaces(res, spaces));
199
+ }
200
+ else {
201
+ console.log(`Created ${opts.type} '${opts.title}' (${res.data.activityId})`);
202
+ console.log(formatSpacesFooter(spaces));
203
+ }
204
+ });
205
+ // -- search ---------------------------------------------------------------
206
+ activity
207
+ .command("search <query>")
208
+ .description("Search activities by title or content")
209
+ .option("--entity <name>", "Filter by entity (name or ID)")
210
+ .option("--type <type>", "Filter by activity type (note, conversation, call, message, task)")
211
+ .option("--limit <n>", "Max results", parseInt)
212
+ .option("--json", "Output raw JSON")
213
+ .action(async (query, opts) => {
214
+ const client = getClient();
215
+ let entityId;
216
+ if (opts.entity) {
217
+ entityId = await resolveEntity(client, opts.entity);
218
+ }
219
+ const [res, spaces] = await Promise.all([
220
+ client.searchActivities(query, {
221
+ entityId,
222
+ type: opts.type,
223
+ limit: opts.limit,
224
+ }),
225
+ client.listSpaces(),
226
+ ]);
227
+ if (opts.json) {
228
+ console.log(jsonOutputWithSpaces(res, spaces));
229
+ }
230
+ else {
231
+ console.log(formatActivityList(res.data));
232
+ console.log(formatSpacesFooter(spaces));
233
+ }
234
+ });
235
+ // -- list -----------------------------------------------------------------
236
+ activity
237
+ .command("list")
238
+ .description("List activities")
239
+ .option("--entity <name>", "Filter by entity (name or ID)")
240
+ .option("--type <type>", "Filter by activity type (task, call, note, message, conversation)")
241
+ .option("--attr <pairs...>", "Filter by attribute key=value")
242
+ .option("--only-open", "Only show non-completed activities")
243
+ .option("--hours <n>", "Only from the last N hours", parseInt)
244
+ .option("--limit <n>", "Max results", parseInt)
245
+ .option("--sort <field>", "Sort by: deadline, created")
246
+ .option("--before <timestamp>", "Only activities before this timestamp (ms)", parseInt)
247
+ .option("--after <timestamp>", "Only activities after this timestamp (ms)", parseInt)
248
+ .option("--json", "Output raw JSON")
249
+ .action(async (opts) => {
250
+ const client = getClient();
251
+ await runActivityList(client, opts);
252
+ });
253
+ // -- get ------------------------------------------------------------------
254
+ activity
255
+ .command("get <id>")
256
+ .description("Get a single activity by ID")
257
+ .option("--json", "Output raw JSON")
258
+ .action(async (id, opts) => {
259
+ const client = getClient();
260
+ const [res, spaces] = await Promise.all([
261
+ client.getActivity(id),
262
+ client.listSpaces(),
263
+ ]);
264
+ if (opts.json) {
265
+ console.log(jsonOutputWithSpaces(res, spaces));
266
+ }
267
+ else {
268
+ console.log(formatActivityDetail(res.data));
269
+ console.log(formatSpacesFooter(spaces));
270
+ }
271
+ });
272
+ // -- update ---------------------------------------------------------------
273
+ activity
274
+ .command("update <id>")
275
+ .description("Update an activity")
276
+ .option("--content <text>", "Update content")
277
+ .option("--title <text>", "Update title")
278
+ .option("--entity-add <names...>", "Link additional entities (resolved by name)")
279
+ .option("--entity-remove <names...>", "Unlink entities (resolved by name)")
280
+ .option("--json", "Output raw JSON")
281
+ .action(async (id, opts) => {
282
+ const client = getClient();
283
+ const body = {};
284
+ if (opts.content !== undefined)
285
+ body.content = opts.content;
286
+ if (opts.title !== undefined)
287
+ body.title = opts.title;
288
+ // Handle entity add/remove by computing the full entityIds list
289
+ if (opts.entityAdd || opts.entityRemove) {
290
+ const activity = await client.getActivity(id);
291
+ const currentIds = activity.data.entityIds ?? [];
292
+ const addIds = opts.entityAdd
293
+ ? await resolveEntities(client, opts.entityAdd)
294
+ : [];
295
+ const removeIds = opts.entityRemove
296
+ ? await resolveEntities(client, opts.entityRemove)
297
+ : [];
298
+ const removeSet = new Set(removeIds);
299
+ const newIds = [
300
+ ...currentIds.filter((eid) => !removeSet.has(eid)),
301
+ ...addIds.filter((eid) => !currentIds.includes(eid)),
302
+ ];
303
+ if (newIds.length === 0) {
304
+ console.error("Cannot remove all entities — an activity must have at least one linked entity.");
305
+ process.exit(1);
306
+ }
307
+ body.entityIds = newIds;
308
+ }
309
+ if (Object.keys(body).length === 0) {
310
+ console.error("Nothing to update. Specify --content, --title, --entity-add, or --entity-remove.");
311
+ process.exit(1);
312
+ }
313
+ const [res, spaces] = await Promise.all([
314
+ client.updateActivity(id, body),
315
+ client.listSpaces(),
316
+ ]);
317
+ if (opts.json) {
318
+ console.log(jsonOutputWithSpaces(res, spaces));
319
+ }
320
+ else {
321
+ console.log(`Updated activity ${id}`);
322
+ console.log(formatSpacesFooter(spaces));
323
+ }
324
+ });
325
+ // -- delete ---------------------------------------------------------------
326
+ activity
327
+ .command("delete <id>")
328
+ .description("Delete an activity")
329
+ .option("--json", "Output JSON")
330
+ .action(async (id, opts) => {
331
+ const client = getClient();
332
+ const [, spaces] = await Promise.all([
333
+ client.deleteActivity(id),
334
+ client.listSpaces(),
335
+ ]);
336
+ if (opts.json) {
337
+ console.log(jsonOutputWithSpaces({ data: { success: true, activityId: id } }, spaces));
338
+ }
339
+ else {
340
+ console.log(`Deleted activity ${id}`);
341
+ console.log(formatSpacesFooter(spaces));
342
+ }
343
+ });
344
+ }
345
+ //# sourceMappingURL=activity.js.map
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ export interface StoredConfig {
3
+ url: string;
4
+ key: string;
5
+ }
6
+ export declare function readStoredConfig(): StoredConfig | null;
7
+ export declare function registerAuth(program: Command): void;
@@ -0,0 +1,86 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "ente");
5
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
6
+ export function readStoredConfig() {
7
+ try {
8
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
9
+ const data = JSON.parse(raw);
10
+ if (data.url && data.key)
11
+ return { url: data.url, key: data.key };
12
+ return null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function writeConfig(config) {
19
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
20
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
21
+ mode: 0o600, // owner read/write only
22
+ });
23
+ }
24
+ export function registerAuth(program) {
25
+ const auth = program.command("auth").description("Manage authentication");
26
+ auth
27
+ .command("login")
28
+ .description("Authenticate with an ente deployment")
29
+ .requiredOption("--url <url>", "Convex site URL (e.g. https://your-deployment.convex.site)")
30
+ .requiredOption("--key <key>", "API key (from ente Settings → API Keys)")
31
+ .action(async (opts) => {
32
+ const url = opts.url.replace(/\/+$/, "");
33
+ // Validate by calling the API
34
+ try {
35
+ const res = await fetch(`${url}/api/external/entities?limit=1`, {
36
+ headers: { Authorization: `Bearer ${opts.key}` },
37
+ });
38
+ if (!res.ok) {
39
+ console.error(`Authentication failed: ${res.status} ${res.statusText}`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+ catch (err) {
44
+ console.error(`Could not reach ${url}: ${err.message}`);
45
+ process.exit(1);
46
+ }
47
+ writeConfig({ url, key: opts.key });
48
+ console.log(`✓ Authenticated successfully`);
49
+ console.log(` Config saved to ${CONFIG_FILE}`);
50
+ });
51
+ auth
52
+ .command("status")
53
+ .description("Show current authentication status")
54
+ .action(() => {
55
+ const envUrl = process.env.ENTE_API_URL;
56
+ const envKey = process.env.ENTE_API_KEY;
57
+ const stored = readStoredConfig();
58
+ if (envUrl && envKey) {
59
+ console.log(`Source: environment variables`);
60
+ console.log(` URL: ${envUrl}`);
61
+ console.log(` Key: ${envKey.slice(0, 12)}...`);
62
+ }
63
+ else if (stored) {
64
+ console.log(`Source: ${CONFIG_FILE}`);
65
+ console.log(` URL: ${stored.url}`);
66
+ console.log(` Key: ${stored.key.slice(0, 12)}...`);
67
+ }
68
+ else {
69
+ console.log("Not authenticated.");
70
+ console.log("Run: ente auth login --url <url> --key <key>");
71
+ }
72
+ });
73
+ auth
74
+ .command("logout")
75
+ .description("Remove stored credentials")
76
+ .action(() => {
77
+ try {
78
+ fs.unlinkSync(CONFIG_FILE);
79
+ console.log("✓ Credentials removed");
80
+ }
81
+ catch {
82
+ console.log("No stored credentials found");
83
+ }
84
+ });
85
+ }
86
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,13 @@
1
+ import { Command } from "commander";
2
+ import type { EnteClient } from "../api.js";
3
+ /**
4
+ * Shared entity-list logic used by both `entity list` and `search`.
5
+ */
6
+ export declare function runEntityList(client: EnteClient, opts: {
7
+ query?: string;
8
+ type?: string;
9
+ limit?: number;
10
+ space?: string;
11
+ json?: boolean;
12
+ }): Promise<void>;
13
+ export declare function registerEntityCommands(program: Command, getClient: () => EnteClient): void;
@@ -0,0 +1,205 @@
1
+ import { resolveEntity, resolveEntities, resolveSpaceByName } from "../resolve.js";
2
+ import { formatEntityDetail, formatEntityList, formatSpacesFooter, jsonOutputWithSpaces, } from "../format.js";
3
+ import { parseAttrs } from "../utils.js";
4
+ /**
5
+ * Shared entity-list logic used by both `entity list` and `search`.
6
+ */
7
+ export async function runEntityList(client, opts) {
8
+ let spaceId;
9
+ if (opts.space) {
10
+ spaceId = await resolveSpaceByName(client, opts.space);
11
+ }
12
+ const res = await client.searchEntities(opts.query, { limit: opts.limit ?? 50, spaceId });
13
+ let entities = res.data.entities;
14
+ if (opts.type) {
15
+ entities = entities.filter((e) => e.entity.type === opts.type);
16
+ }
17
+ if (opts.json) {
18
+ console.log(jsonOutputWithSpaces({ data: { ...res.data, entities } }, res.data.spaces));
19
+ }
20
+ else {
21
+ console.log(formatEntityList(entities, res.data.spaces));
22
+ console.log(formatSpacesFooter(res.data.spaces));
23
+ }
24
+ }
25
+ export function registerEntityCommands(program, getClient) {
26
+ const entity = program
27
+ .command("entity")
28
+ .description("Manage entities");
29
+ // -- list -----------------------------------------------------------------
30
+ entity
31
+ .command("list")
32
+ .description("List or search entities")
33
+ .option("--query <text>", "Fuzzy name search")
34
+ .option("--type <type>", "Filter by entity type (person, org, project, topic)")
35
+ .option("--space <name-or-id>", "Filter by space (name or ID)")
36
+ .option("--limit <n>", "Max results (default: 50)", parseInt)
37
+ .option("--json", "Output raw JSON")
38
+ .action(async (opts) => {
39
+ const client = getClient();
40
+ await runEntityList(client, opts);
41
+ });
42
+ // -- create ---------------------------------------------------------------
43
+ entity
44
+ .command("create <name>")
45
+ .description("Create a new entity")
46
+ .requiredOption("--type <type>", "Entity type (person, org, project, topic)")
47
+ .option("--pin <entities...>", "Pin to other entities (resolved by name)")
48
+ .option("--alias <aliases...>", "Alternative names")
49
+ .option("--attr <pairs...>", "Attributes as key=value pairs")
50
+ .option("--description <text>", "Description text")
51
+ .option("--json", "Output raw JSON")
52
+ .action(async (name, opts) => {
53
+ const client = getClient();
54
+ const attributes = parseAttrs(opts.attr);
55
+ const res = await client.createEntity({
56
+ name,
57
+ type: opts.type,
58
+ description: opts.description,
59
+ aliases: opts.alias,
60
+ attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
61
+ });
62
+ const entityId = res.data.entityId;
63
+ // Pin targets
64
+ if (opts.pin && opts.pin.length > 0) {
65
+ const pinIds = await resolveEntities(client, opts.pin);
66
+ for (const targetId of pinIds) {
67
+ await client.addPin(entityId, targetId);
68
+ }
69
+ }
70
+ const spaces = await client.listSpaces();
71
+ if (opts.json) {
72
+ console.log(jsonOutputWithSpaces(res, spaces));
73
+ }
74
+ else {
75
+ console.log(`Created ${opts.type} '${name}' (${entityId})`);
76
+ if (opts.pin && opts.pin.length > 0) {
77
+ console.log(`Pinned to: ${opts.pin.join(", ")}`);
78
+ }
79
+ console.log(formatSpacesFooter(spaces));
80
+ }
81
+ });
82
+ // -- get ------------------------------------------------------------------
83
+ entity
84
+ .command("get <name-or-id>")
85
+ .description("Get entity details")
86
+ .option("--json", "Output raw JSON")
87
+ .action(async (nameOrId, opts) => {
88
+ const client = getClient();
89
+ const id = await resolveEntity(client, nameOrId);
90
+ const [res, spaces] = await Promise.all([
91
+ client.getEntity(id, { limit: 10 }),
92
+ client.listSpaces(),
93
+ ]);
94
+ if (opts.json) {
95
+ console.log(jsonOutputWithSpaces(res, spaces));
96
+ }
97
+ else {
98
+ console.log(formatEntityDetail(res.data));
99
+ console.log(formatSpacesFooter(spaces));
100
+ }
101
+ });
102
+ // -- update ---------------------------------------------------------------
103
+ entity
104
+ .command("update <name-or-id>")
105
+ .description("Update an entity")
106
+ .option("--alias-add <alias>", "Add an alias")
107
+ .option("--alias-remove <alias>", "Remove an alias")
108
+ .option("--attr <pairs...>", "Set attributes as key=value pairs")
109
+ .option("--description <text>", "Update description")
110
+ .option("--json", "Output raw JSON")
111
+ .action(async (nameOrId, opts) => {
112
+ const client = getClient();
113
+ const id = await resolveEntity(client, nameOrId);
114
+ const body = {};
115
+ const newAttrs = parseAttrs(opts.attr);
116
+ const needsFetch = opts.aliasAdd || opts.aliasRemove || Object.keys(newAttrs).length > 0;
117
+ // Fetch current entity once if we need to merge aliases or attributes
118
+ const currentEntity = needsFetch ? (await client.getEntity(id)).data.entity : null;
119
+ if (opts.aliasAdd || opts.aliasRemove) {
120
+ const aliases = [...(currentEntity?.aliases || [])];
121
+ if (opts.aliasAdd && !aliases.includes(opts.aliasAdd)) {
122
+ aliases.push(opts.aliasAdd);
123
+ }
124
+ if (opts.aliasRemove) {
125
+ const idx = aliases.indexOf(opts.aliasRemove);
126
+ if (idx !== -1)
127
+ aliases.splice(idx, 1);
128
+ }
129
+ body.aliases = aliases;
130
+ }
131
+ if (Object.keys(newAttrs).length > 0) {
132
+ // Server does full replace for entity attributes, so merge with existing
133
+ const existing = currentEntity?.attributes ?? {};
134
+ body.attributes = { ...existing, ...newAttrs };
135
+ }
136
+ if (opts.description !== undefined) {
137
+ body.description = opts.description;
138
+ }
139
+ if (Object.keys(body).length === 0) {
140
+ console.error("Nothing to update. Specify --alias-add, --alias-remove, --attr, or --description.");
141
+ process.exit(1);
142
+ }
143
+ const [res, spaces] = await Promise.all([
144
+ client.updateEntity(id, body),
145
+ client.listSpaces(),
146
+ ]);
147
+ if (opts.json) {
148
+ console.log(jsonOutputWithSpaces(res, spaces));
149
+ }
150
+ else {
151
+ console.log(`Updated entity ${id}`);
152
+ console.log(formatSpacesFooter(spaces));
153
+ }
154
+ });
155
+ // -- pin / unpin ----------------------------------------------------------
156
+ entity
157
+ .command("pin <source> <target>")
158
+ .description("Pin target entity to source entity")
159
+ .option("--json", "Output JSON")
160
+ .action(async (source, target, opts) => {
161
+ const client = getClient();
162
+ const [sourceId, targetId] = await Promise.all([
163
+ resolveEntity(client, source),
164
+ resolveEntity(client, target),
165
+ ]);
166
+ const [res, spaces] = await Promise.all([
167
+ client.addPin(sourceId, targetId),
168
+ client.listSpaces(),
169
+ ]);
170
+ if (opts.json) {
171
+ console.log(jsonOutputWithSpaces(res, spaces));
172
+ }
173
+ else if (res.data.alreadyExists) {
174
+ console.log("Pin already exists.");
175
+ console.log(formatSpacesFooter(spaces));
176
+ }
177
+ else {
178
+ console.log("Pin added.");
179
+ console.log(formatSpacesFooter(spaces));
180
+ }
181
+ });
182
+ entity
183
+ .command("unpin <source> <target>")
184
+ .description("Remove pin between entities")
185
+ .option("--json", "Output JSON")
186
+ .action(async (source, target, opts) => {
187
+ const client = getClient();
188
+ const [sourceId, targetId] = await Promise.all([
189
+ resolveEntity(client, source),
190
+ resolveEntity(client, target),
191
+ ]);
192
+ const [, spaces] = await Promise.all([
193
+ client.removePin(sourceId, targetId),
194
+ client.listSpaces(),
195
+ ]);
196
+ if (opts.json) {
197
+ console.log(jsonOutputWithSpaces({ data: { success: true } }, spaces));
198
+ }
199
+ else {
200
+ console.log("Pin removed.");
201
+ console.log(formatSpacesFooter(spaces));
202
+ }
203
+ });
204
+ }
205
+ //# sourceMappingURL=entity.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { EnteClient } from "../api.js";
3
+ export declare function registerSearchCommand(program: Command, getClient: () => EnteClient): void;
@@ -0,0 +1,13 @@
1
+ import { runEntityList } from "./entity.js";
2
+ export function registerSearchCommand(program, getClient) {
3
+ program
4
+ .command("search <query>")
5
+ .description("Fuzzy search entities")
6
+ .option("--type <type>", "Filter by entity type (person, org, project, topic)")
7
+ .option("--json", "Output raw JSON")
8
+ .action(async (query, opts) => {
9
+ const client = getClient();
10
+ await runEntityList(client, { query, type: opts.type, json: opts.json });
11
+ });
12
+ }
13
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { EnteClient } from "../api.js";
3
+ export declare function registerTaskCommands(program: Command, getClient: () => EnteClient): void;