@parall/cli 1.55.5 → 1.56.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.
@@ -1 +1 @@
1
- {"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmFpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA+fpD"}
1
+ {"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwLpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA2jBpD"}
@@ -52,6 +52,87 @@ async function loadDeviceIndex(client, orgId) {
52
52
  return null;
53
53
  }
54
54
  }
55
+ const CLIP_REF_HELP = 'Registry clip id (crg_...), an exact clip name (your org first, then a unique public match), or org_<id>/<name> to pin the publisher (discover ids with `parall clip search`)';
56
+ /**
57
+ * The clip-ref grammar shared with the standalone Solo CLI
58
+ * (desktop/edge/cli/parall-clip.mjs, parseClipRef) — the two install surfaces
59
+ * must read the same ref the same way. Any slash makes a namespaced ref, and
60
+ * only org ids are authoritative namespaces; throws on a malformed ref.
61
+ */
62
+ function parseClipRef(ref) {
63
+ if (ref.startsWith('crg_'))
64
+ return { id: ref };
65
+ const slash = ref.indexOf('/');
66
+ if (slash === -1)
67
+ return { name: ref };
68
+ const namespace = ref.slice(0, slash);
69
+ const name = ref.slice(slash + 1);
70
+ if (!namespace || !name || name.includes('/')) {
71
+ throw new Error(`Invalid clip reference "${ref}". ${CLIP_REF_HELP}.`);
72
+ }
73
+ if (!namespace.startsWith('org_')) {
74
+ throw new Error(`Publisher namespace "${namespace}" is not authoritative yet. Use an immutable crg_ id or org_<id>/${name}.`);
75
+ }
76
+ return { namespace, name };
77
+ }
78
+ /**
79
+ * Resolve an install/info argument to a registry entry with per-name queries
80
+ * only — never by listing the catalog, which spans every public clip on the
81
+ * platform. Bare names resolve in the CALLER'S org first (an authoritative
82
+ * point read immune to a third party publishing the same name), then fall
83
+ * back to a unique exact match among visible public clips; several publishers
84
+ * sharing the name is an explicit ambiguity error, never "first match wins".
85
+ * Returns null on a clean not-found; non-404 errors (e.g. 403
86
+ * CLIP_NOT_APPROVED) propagate untouched.
87
+ */
88
+ async function resolveRegistryRef(client, orgId, ref) {
89
+ const notFoundToNull = (err) => {
90
+ if (err.status === 404)
91
+ return null;
92
+ throw err;
93
+ };
94
+ const parsed = parseClipRef(ref);
95
+ if ('id' in parsed) {
96
+ return client.getRegistryClip(orgId, parsed.id).catch(notFoundToNull);
97
+ }
98
+ if ('namespace' in parsed) {
99
+ return client.resolveRegistryClip(orgId, parsed.namespace, parsed.name).catch(notFoundToNull);
100
+ }
101
+ const own = await client.resolveRegistryClip(orgId, orgId, parsed.name).catch(notFoundToNull);
102
+ if (own)
103
+ return own;
104
+ // Bare name, not published by the caller's org: accept a UNIQUE exact match
105
+ // among visible clips. Search is fuzzy (name+description substrings), so
106
+ // exact-filter its max-size page; a page-sized crowd of lookalikes could
107
+ // still hide an exact match — the crg_ id and org_<id>/<name> forms stay
108
+ // the always-sound addresses. A 404 here is a server without the search
109
+ // route, not a lookup miss — degrade to "no matches" so the caller gets
110
+ // the guidance error instead of a bare route error.
111
+ const page = await client
112
+ .searchOrgRegistryClips(orgId, parsed.name, { limit: 100 })
113
+ .catch((err) => {
114
+ if (err.status === 404)
115
+ return [];
116
+ throw err;
117
+ });
118
+ const matches = page.filter((c) => c.name === parsed.name);
119
+ if (matches.length > 1) {
120
+ const candidates = matches.map((c) => `${c.org_id}/${c.name} (${c.id})`).join(', ');
121
+ throw new Error(`Clip name "${parsed.name}" is ambiguous across publishers: ${candidates}. Use the crg_ id or the org_<id>/<name> form.`);
122
+ }
123
+ // A FULL page proves nothing about uniqueness or absence: rows order by
124
+ // (name, id), so the exact-name block can straddle the page boundary and a
125
+ // single visible match — or none — may be a truncation artifact. Two exact
126
+ // matches on the page prove ambiguity outright (handled above); otherwise
127
+ // abstain rather than risk installing the wrong publisher's clip.
128
+ if (page.length >= 100) {
129
+ throw new Error(`Too many clips have names or descriptions similar to "${parsed.name}" for a bare name to resolve safely. Pass the crg_ id or the org_<id>/<name> form (find them with \`parall clip search <text>\`).`);
130
+ }
131
+ return matches[0] ?? null;
132
+ }
133
+ function clipNotFoundError(ref) {
134
+ return new Error(`No registry clip matching "${ref}". Pass a crg_ id, the exact name of a visible clip, or org_<id>/<name> to pin its publisher — discover ids with \`parall clip search <text>\`.`);
135
+ }
55
136
  export function registerClipCommands(program) {
56
137
  const clip = program
57
138
  .command('clip')
@@ -101,22 +182,70 @@ export function registerClipCommands(program) {
101
182
  printError(err);
102
183
  }
103
184
  });
185
+ clip
186
+ .command('search')
187
+ .description('Search the visible clip registry server-side (your org + cross-org public clips) — rows carry the crg_ id that `clip install` takes')
188
+ .argument('<query>', 'Text matched against clip names and descriptions (substring, case-insensitive)')
189
+ .option('--limit <n>', 'Rows per page, up to 100 (server default 50)')
190
+ .option('--offset <n>', 'Rows to skip — page through large result sets')
191
+ .action(async (query, opts) => {
192
+ try {
193
+ const { client, orgId } = resolveCredentials();
194
+ // Strict grammar BEFORE the wire (same rationale as exec --timeout):
195
+ // the server silently clamps malformed or out-of-range values, which
196
+ // would page differently than the caller asked.
197
+ const limit = parsePositiveInt(opts?.limit);
198
+ if (opts?.limit !== undefined && (limit === undefined || limit > 100)) {
199
+ printError(new Error('--limit must be an integer between 1 and 100'));
200
+ return;
201
+ }
202
+ // Unlike limit, 0 is a valid offset (the explicit first page), so
203
+ // parsePositiveInt alone would reject a legal boundary value.
204
+ let offset;
205
+ if (opts?.offset !== undefined) {
206
+ offset = opts.offset.trim() === '0' ? 0 : parsePositiveInt(opts.offset);
207
+ if (offset === undefined) {
208
+ printError(new Error('--offset must be a non-negative integer'));
209
+ return;
210
+ }
211
+ }
212
+ const entries = await client.searchOrgRegistryClips(orgId, query, { limit, offset });
213
+ if (entries.length === 0) {
214
+ printJson({ data: [], message: `No registry clips matched "${query}"` });
215
+ return;
216
+ }
217
+ // publisher tells the caller which install form works: rows from your
218
+ // own org install by bare name; cross-org rows need org_<id>/<name>
219
+ // (publisher IS that org id) or the crg_ id.
220
+ printJson(entries.map((c) => ({
221
+ id: c.id,
222
+ name: c.name,
223
+ publisher: c.org_id === orgId ? 'your org' : c.org_id,
224
+ ...(c.description ? { description: c.description } : {}),
225
+ version: c.version ?? '-',
226
+ visibility: c.visibility,
227
+ })));
228
+ }
229
+ catch (err) {
230
+ printError(err);
231
+ }
232
+ });
104
233
  clip
105
234
  .command('install')
106
235
  .description('Install a registry clip into the organization')
107
- .argument('<clip>', 'Registry clip id (crg_...) or name')
236
+ .argument('<clip>', CLIP_REF_HELP)
108
237
  .action(async (clip) => {
109
238
  try {
110
239
  const { client, orgId } = resolveCredentials();
111
240
  // v3 installs a REFERENCE into clip_installs, keyed by the registry id.
112
241
  // There is no machine to pick: a v3 clip runs wherever the connection
113
242
  // it is bound to points, which is why the old --machine-id is gone.
114
- // listOrgRegistryClips is the surface carrying `crg_` ids — the Pinix
115
- // catalog proxy does not.
116
- const registry = await client.listOrgRegistryClips(orgId);
117
- const found = registry.find((c) => c.id === clip || c.name === clip);
243
+ // Resolution is a point query — the visible catalog spans every
244
+ // public clip on the platform, so it must never be listed to find one
245
+ // (the old list+find lost every clip past the first page).
246
+ const found = await resolveRegistryRef(client, orgId, clip);
118
247
  if (!found) {
119
- printError(new Error(`No registry clip matching "${clip}". Pass a crg_ id or the exact registry name; a private clip from another org is not installable.`));
248
+ printError(clipNotFoundError(clip));
120
249
  return;
121
250
  }
122
251
  // Idempotent server-side: installing an already-installed clip is a no-op.
@@ -130,48 +259,52 @@ export function registerClipCommands(program) {
130
259
  clip
131
260
  .command('info')
132
261
  .description("Show detailed clip information, including its manifest commands and params. For your org's OWN clips this includes review_status/review_note (the platform-review state of a public publish)")
133
- .argument('<clip>', 'Registry clip id (crg_...) or name')
262
+ .argument('<clip>', `Installed clip id or name, or — for uninstalled clips — ${CLIP_REF_HELP}`)
134
263
  .action(async (clip) => {
135
264
  try {
136
265
  const { client, orgId } = resolveCredentials();
137
- // Resolve installed-first (the historical semantic), then fall back to
138
- // the org registry: an author checking a clip they published but never
266
+ // Resolve installed-first (the historical semantic; the installed list
267
+ // is org-local and uncapped), then fall back to point-querying the
268
+ // registry: an author checking a clip they published but never
139
269
  // installed — the publish→review→revise loop — must not dead-end here.
270
+ // The installed shortcut obeys the SAME bare-name contract as
271
+ // resolveRegistryRef: an org can install same-name clips from several
272
+ // publishers (installs are keyed by id), so a plain first-match would
273
+ // show an arbitrary one — own org wins, and a foreign tie is the same
274
+ // explicit ambiguity error, never a silent pick.
140
275
  const installed = await client.listInstalledRegistryClips(orgId);
141
- let found = installed.find((c) => c.id === clip || c.name === clip);
142
- if (!found) {
143
- const registry = await client.listOrgRegistryClips(orgId);
144
- found = registry.find((c) => c.id === clip || c.name === clip);
145
- }
146
- let entry = found;
147
- if (found) {
148
- // The detail read is what carries review_status/review_note (server
149
- // attaches them only for the owning org). A cross-org entry can lose
150
- // detail visibility between the list and this read — the list entry
151
- // is still correct data, so fall back to it rather than failing info.
152
- try {
153
- entry = await client.getRegistryClip(orgId, found.id);
154
- }
155
- catch {
156
- entry = found;
157
- }
276
+ const byId = installed.find((c) => c.id === clip);
277
+ const byName = byId ? [] : installed.filter((c) => c.name === clip);
278
+ let found = byId ?? byName.find((c) => c.org_id === orgId);
279
+ if (!found && byName.length > 1) {
280
+ const candidates = byName.map((c) => `${c.org_id}/${c.name} (${c.id})`).join(', ');
281
+ printError(new Error(`Several installed clips are named "${clip}": ${candidates}. Use the crg_ id or the org_<id>/<name> form.`));
282
+ return;
158
283
  }
159
- else if (clip.startsWith('crg_')) {
160
- // Both lists are single pages (registry capped at 100 entries), so a
161
- // valid id can sit beyond them an exact crg_ id needs no list
162
- // resolution at all; go straight to the detail endpoint before
163
- // concluding not-found.
284
+ found ??= byName[0];
285
+ let entry = found ?? (await resolveRegistryRef(client, orgId, clip));
286
+ // The detail read is what carries review_status/review_note (server
287
+ // attaches them only for the owning org; the resolve read does not).
288
+ // resolveRegistryRef's crg_ branch IS that detail read already. A
289
+ // cross-org entry can lose detail visibility between the lookup and
290
+ // this read (a 404) — the lookup entry is still correct data, so fall
291
+ // back to it rather than failing info. Any OTHER failure (auth, 5xx,
292
+ // network) must surface: silently printing partial data would report
293
+ // a broken read as success.
294
+ const alreadyDetail = !found && clip.startsWith('crg_');
295
+ if (entry && !alreadyDetail) {
164
296
  try {
165
- entry = await client.getRegistryClip(orgId, clip);
297
+ entry = await client.getRegistryClip(orgId, entry.id);
166
298
  }
167
- catch {
168
- // fall through to the not-found error below
299
+ catch (err) {
300
+ if (err.status !== 404)
301
+ throw err;
169
302
  }
170
303
  }
171
304
  if (!entry) {
172
305
  // `return` matters: without it the lookup falls through and dies
173
306
  // with a TypeError instead of the message just printed.
174
- printError(new Error(`No clip matching "${clip}" (see \`parall clip list\`)`));
307
+ printError(clipNotFoundError(clip));
175
308
  return;
176
309
  }
177
310
  const info = {
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5C,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GACtD;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CASzC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAoPvD"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5C,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GACtD;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CASzC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAkSvD"}
@@ -245,4 +245,47 @@ export function registerMessageCommands(program) {
245
245
  printError(err);
246
246
  }
247
247
  });
248
+ // ---- Thread watchers ----
249
+ messages
250
+ .command('watch')
251
+ .description('Watch a thread (subscribe to its replies)')
252
+ .argument('<threadRootId>', 'Thread root message ID')
253
+ .action(async (threadRootId) => {
254
+ try {
255
+ const { client } = resolveCredentials();
256
+ await client.watchThread(stripPrllScheme(threadRootId));
257
+ printJson({ ok: true });
258
+ }
259
+ catch (err) {
260
+ printError(err);
261
+ }
262
+ });
263
+ messages
264
+ .command('unwatch')
265
+ .description('Unwatch a thread (stop receiving its replies; sticky — only replying or watch re-subscribes)')
266
+ .argument('<threadRootId>', 'Thread root message ID')
267
+ .action(async (threadRootId) => {
268
+ try {
269
+ const { client } = resolveCredentials();
270
+ await client.unwatchThread(stripPrllScheme(threadRootId));
271
+ printJson({ ok: true });
272
+ }
273
+ catch (err) {
274
+ printError(err);
275
+ }
276
+ });
277
+ messages
278
+ .command('watchers')
279
+ .description('List watchers of a thread')
280
+ .argument('<threadRootId>', 'Thread root message ID')
281
+ .action(async (threadRootId) => {
282
+ try {
283
+ const { client } = resolveCredentials();
284
+ const result = await client.getThreadWatchers(stripPrllScheme(threadRootId));
285
+ printJson(result);
286
+ }
287
+ catch (err) {
288
+ printError(err);
289
+ }
290
+ });
248
291
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiEpC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;;;;;;;;;;GAaA;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;YAS0B,iBAAiB,CAAC,QAAQ,CAAC;cACvB,iBAAiB,CAAC,UAAU,CAAC;;;;;;;;GAS3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAkXpD"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiEpC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;;;;;;;;;;GAaA;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;YAS0B,iBAAiB,CAAC,QAAQ,CAAC;cACvB,iBAAiB,CAAC,UAAU,CAAC;;;;;;;;GAS3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA0YpD"}
@@ -170,6 +170,25 @@ export function registerTaskCommands(program) {
170
170
  printError(err);
171
171
  }
172
172
  });
173
+ tasks
174
+ .command('move')
175
+ .description('Move a task (and its whole subtree) to another project. The subtree is renumbered from the target project, old KEY-N identifiers stay resolvable as aliases, and subscribers who cannot read the target project lose their subscription. Requires update access on the task (source project membership) and read access to the target project.')
176
+ .argument('<taskId>', 'Task ID')
177
+ .requiredOption('--project <projectId>', 'Target project ID')
178
+ .option('--parent <taskId>', 'Attach under this parent task in the target project (omit to land top-level)')
179
+ .action(async (taskId, opts) => {
180
+ try {
181
+ const { client, orgId } = resolveCredentials();
182
+ const result = await client.moveTask(orgId, stripPrllScheme(taskId), {
183
+ project_id: stripPrllScheme(opts.project),
184
+ ...(opts.parent ? { parent_id: stripPrllScheme(opts.parent) } : {}),
185
+ });
186
+ printJson(result);
187
+ }
188
+ catch (err) {
189
+ printError(err);
190
+ }
191
+ });
173
192
  tasks
174
193
  .command('update')
175
194
  .description('Update a task')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.55.5",
3
+ "version": "1.56.0",
4
4
  "description": "CLI client for Parall — universal agent & human access to Parall API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,14 +36,14 @@
36
36
  "diff": "^8.0.3",
37
37
  "js-yaml": "^4.1.0",
38
38
  "zod": "^4.3.6",
39
- "@parall/agent-core": "1.55.5",
40
- "@parall/sdk": "1.55.5"
39
+ "@parall/agent-core": "1.56.0",
40
+ "@parall/sdk": "1.56.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/js-yaml": "^4.0.9",
44
44
  "@types/node": "^22.0.0",
45
45
  "typescript": "^5.7.0",
46
- "@parall/agent-core": "1.55.5"
46
+ "@parall/agent-core": "1.56.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc",