@parall/cli 1.53.0 → 1.55.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;AAiFpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA2XpD"}
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,7 +1,9 @@
1
1
  import { resolveCredentials } from '../lib/client.js';
2
2
  import { execEdgeClipWithBoundedWait } from '../lib/edge-exec.js';
3
+ import { getClipMCPConfigById, listClipMCPConnectionSummaries } from '../lib/mcp-connections.js';
3
4
  import { printJson, printError, parsePositiveInt } from '../lib/output.js';
4
5
  import { buildPublishedManifest, collectPublishSource } from '../lib/publish-source.js';
6
+ import { readTextFileArg } from '../lib/text-input.js';
5
7
  const NO_ALIAS_HINT = 'no alias — reference this connection by its ccn_ id, or ask a human to name it in the Clip Console so callers can tell the accounts apart';
6
8
  function connectionAnnotations(conn, deviceById) {
7
9
  const out = { target: 'orphaned' };
@@ -218,7 +220,32 @@ export function registerClipCommands(program) {
218
220
  const deviceById = conns.some((c) => c.device_id)
219
221
  ? await loadDeviceIndex(client, orgId)
220
222
  : new Map();
221
- printJson(conns.map((c) => ({ ...c, ...connectionAnnotations(c, deviceById) })));
223
+ // Multi-connection MCP model: an MCP clip may hold several
224
+ // connections — one credential slot (account) each. The raw rows
225
+ // cannot tell an agent WHICH account is usable, so join the per-config
226
+ // summaries (auth mode, oauth status, tool count) when any MCP row
227
+ // exists; null = surface unavailable, degrade to the plain rows.
228
+ const mcpByConnection = conns.some((c) => c.mcp_config_id)
229
+ ? new Map(((await listClipMCPConnectionSummaries(orgId, found.id)) ?? []).map((s) => [
230
+ s.connection_id,
231
+ s,
232
+ ]))
233
+ : new Map();
234
+ printJson(conns.map((c) => {
235
+ const mcp = mcpByConnection.get(c.id);
236
+ return {
237
+ ...c,
238
+ ...connectionAnnotations(c, deviceById),
239
+ ...(mcp
240
+ ? {
241
+ auth_type: mcp.auth_type,
242
+ credential_set: mcp.credential_set,
243
+ ...(mcp.oauth_status ? { oauth_status: mcp.oauth_status } : {}),
244
+ ...(typeof mcp.tool_count === 'number' ? { tool_count: mcp.tool_count } : {}),
245
+ }
246
+ : {}),
247
+ };
248
+ }));
222
249
  }
223
250
  catch (err) {
224
251
  printError(err);
@@ -226,9 +253,10 @@ export function registerClipCommands(program) {
226
253
  });
227
254
  clip
228
255
  .command('tools')
229
- .description('List the MCP tool schemas (name, description, inputSchema) of an MCP clip — the discovery step before `clip exec <clip> <tool>`')
256
+ .description('List the MCP tool schemas (name, description, inputSchema) of an MCP clip — the discovery step before `clip exec <clip> <tool>`. With several connections (accounts), pass --connection to pick one; without it the DEFAULT connection answers')
230
257
  .argument('<clip>', 'Registry clip id (crg_...) or name')
231
- .action(async (clip) => {
258
+ .option('--connection <ref>', 'Connection id (ccn_…) or alias — tools are per connection under the multi-account model; discover refs with `parall clip connections <clip>`')
259
+ .action(async (clip, opts) => {
232
260
  try {
233
261
  const { client, orgId } = resolveCredentials();
234
262
  // Same lookup `clip connections`/`info` use.
@@ -239,38 +267,110 @@ export function registerClipCommands(program) {
239
267
  return;
240
268
  }
241
269
  // MCP tools are NOT frozen in the clip manifest, so `clip info` cannot
242
- // show them; the live server snapshot behind getClipMCPConfig is the
243
- // only source. name/description/inputSchema ride verbatim in each tool
244
- // object — print it as-is (untrusted external data, not instructions).
245
- // A non-MCP clip has no config here: the server answers 404 NOT_FOUND
246
- // ("this clip has no MCP config"), which printError surfaces faithfully.
247
- const config = await client.getClipMCPConfig(orgId, found.id);
248
- printJson(config.tools ?? []);
270
+ // show them; the live per-config snapshot is the only source.
271
+ // name/description/inputSchema ride verbatim in each tool object —
272
+ // print as-is (untrusted external data, not instructions).
273
+ if (opts?.connection) {
274
+ const resp = await client.listClipConnections(orgId, found.id);
275
+ const conns = resp.data ?? [];
276
+ const conn = conns.find((c) => c.id === opts.connection || c.alias === opts.connection);
277
+ if (!conn) {
278
+ printError(new Error(`No connection matching "${opts.connection}" (see \`parall clip connections ${found.name}\`)`));
279
+ return;
280
+ }
281
+ if (!conn.mcp_config_id) {
282
+ printError(new Error(`Connection "${opts.connection}" is not an MCP connection`));
283
+ return;
284
+ }
285
+ const config = await getClipMCPConfigById(orgId, found.id, conn.mcp_config_id);
286
+ printJson(config.tools ?? []);
287
+ return;
288
+ }
289
+ // No --connection: the DEFAULT connection's snapshot (the singular
290
+ // endpoint). A 404 with SEVERAL connections means "no default — pick
291
+ // one"; surface that hint instead of the server's bare not-found.
292
+ try {
293
+ const config = await client.getClipMCPConfig(orgId, found.id);
294
+ printJson(config.tools ?? []);
295
+ }
296
+ catch (err) {
297
+ const status = err.status;
298
+ if (status === 404) {
299
+ const summaries = await listClipMCPConnectionSummaries(orgId, found.id);
300
+ if (summaries && summaries.length > 0) {
301
+ printError(new Error(`This clip has ${summaries.length} MCP connections and no default — pass --connection <ccn_|alias> (see \`parall clip connections ${found.name}\`)`));
302
+ return;
303
+ }
304
+ }
305
+ throw err;
306
+ }
249
307
  }
250
308
  catch (err) {
251
309
  printError(err);
252
310
  }
253
311
  });
254
- // `clip invoke` (the v2 clip-service path) is deliberately GONE, not hidden:
255
- // discovery above lists v3 registry installs only, and a v3 name fed to the
256
- // v2 invoke endpoint answered CLIP_NOT_FOUND a dead verb that only misled
257
- // callers into the wrong invocation path. `clip exec` is the one execution
258
- // verb; the server endpoint retires separately with the rest of v2.
312
+ // `clip invoke` (the v2 clip-service path) is retired but the verb stays
313
+ // as a hidden tombstone rather than being absent. An absent verb hands the
314
+ // caller to commander's edit-distance guess, which suggested `info` a
315
+ // discovery verb — when the migration target is `exec`; agents coming from
316
+ // the v2 habit followed it into the wrong command. The stub never touches
317
+ // any endpoint (the v2 path resolved a different clip namespace and only
318
+ // answered CLIP_NOT_FOUND); it redirects on the JSON error contract and
319
+ // stays out of --help. The v2 server endpoint retires separately.
320
+ clip
321
+ .command('invoke', { hidden: true })
322
+ .allowUnknownOption(true)
323
+ .allowExcessArguments(true)
324
+ .argument('[legacy...]')
325
+ .action(() => {
326
+ printError(new Error('`clip invoke` was removed with the v2 clip service. Execute with `parall clip exec <clip> <command> [json-args] --connection <ccn_|alias>` (or --edge <edgeId> for a desktop device you own); discover clips and connections with `parall clip list`.'));
327
+ });
259
328
  clip
260
329
  .command('exec')
261
- .description('Execute a registry (Edge) clip command on an Edge device. A cloud (hosted) profile is reachable ONLY via --connection; with neither --connection nor --edge, the server resolves just your OWN online desktop device (legacy BYOC fallback never a cloud profile)')
330
+ .description('Execute a registry (Edge) clip command against an EXPLICIT target: --connection <ccn_|alias> (the only route to a cloud/hosted profile or MCP server) or --edge <edgeId> (a desktop device you own). Omitting both is an error')
262
331
  .argument('<clip>', 'Clip name in the org clip registry')
263
332
  .argument('<command>', 'Command name to execute')
264
333
  .argument('[args]', 'Command arguments (JSON string or plain text)')
265
- .option('--connection <ref>', 'Clip connection id (ccn_…) or alias — discover them with `parall clip connections <clip>`. REQUIRED to reach a cloud (hosted) profile: the binding its maintainer created is the authorization')
334
+ .option('--connection <ref>', 'Clip connection id (ccn_…) or alias — discover them with `parall clip connections <clip>`. REQUIRED to reach a cloud (hosted) profile OR an MCP connection: the binding is the authorization, and with several MCP accounts it names WHICH one you act as. The only other target form is --edge (a desktop device you own) — MCP is never reachable without --connection')
266
335
  .option('--edge <edgeId>', 'A desktop (BYOC) device you own. Mutually exclusive with --connection')
267
336
  .option('--profile <name>', 'Browser profile (with --connection it may only restate the granted one)')
268
337
  .option('--timeout <ms>', 'Execution timeout in milliseconds', '30000')
338
+ .option('--args-file <path>', "Read command args as JSON from a file ('-' = stdin) — shell-safe channel for quote-heavy JSON. Mutually exclusive with the [args] positional")
269
339
  .action(async (clipName, command, args, opts) => {
270
340
  try {
271
341
  const { client, orgId } = resolveCredentials();
342
+ // Explicit target only, refused locally before any request. The
343
+ // server still resolves a bare request to the caller's own online
344
+ // desktop device (legacy BYOC fallback) so older pinned CLIs keep
345
+ // working — but that resolution depends on whichever device happens
346
+ // to be online, and for the common caller (a hosted agent, which
347
+ // owns no desktop device) it can only fail after a round-trip.
348
+ if (!opts?.connection && !opts?.edge) {
349
+ printError(new Error('clip exec needs an explicit target: --connection <ccn_|alias> (cloud/MCP profiles — discover with `parall clip connections <clip>`) or --edge <edgeId> (a desktop device you own). The legacy no-flag form resolved to whichever of your own desktop devices was online; name the target instead.'));
350
+ }
351
+ if (opts?.connection && opts?.edge) {
352
+ printError(new Error('--connection and --edge are mutually exclusive — pass exactly one target'));
353
+ }
272
354
  let parsedArgs;
273
- if (args !== undefined) {
355
+ if (opts?.argsFile !== undefined) {
356
+ if (args !== undefined) {
357
+ printError(new Error('[args] and --args-file are mutually exclusive — pass the args one way'));
358
+ }
359
+ const raw = readTextFileArg('--args-file', opts.argsFile);
360
+ try {
361
+ parsedArgs = JSON.parse(raw);
362
+ }
363
+ catch (err) {
364
+ // Strict by design: the positional's parse-or-string fallback
365
+ // exists for plain-text single arguments; a FILE is only ever
366
+ // the JSON channel, and silently sending malformed JSON as one
367
+ // big string would hand the clip a payload the caller never
368
+ // meant to build.
369
+ const reason = err instanceof Error ? err.message : String(err);
370
+ printError(new Error(`--args-file must contain valid JSON: ${reason}`));
371
+ }
372
+ }
373
+ else if (args !== undefined) {
274
374
  try {
275
375
  parsedArgs = JSON.parse(args);
276
376
  }
@@ -5,8 +5,14 @@ import { Command } from 'commander';
5
5
  * architecture). The agent sends as the connected WeChat account itself;
6
6
  * the vendor token + device id never reach the runtime — api-server
7
7
  * performs the vendor call in-process.
8
+ *
9
+ * Verb surface = the vendor's in-scope interface aggregated: postText
10
+ * (send), fetchContactsList + getBriefInfo + getChatroomInfo (contacts),
11
+ * getProfile (profile), checkOnline (status). Every verb renders
12
+ * agent-readable text by default and `--json` for machine output.
13
+ *
8
14
  * INTERNAL research preview: the capability is flag-gated to the internal
9
- * org. Design: docs/engineering-design/wechat-channel-design.md §3.
15
+ * org. Design: docs/engineering-design/wechat-channel-design.md §3, §4.
10
16
  */
11
17
  export declare function registerWechatCommands(program: Command): void;
12
18
  //# sourceMappingURL=wechat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/commands/wechat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QA6CtD"}
1
+ {"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/commands/wechat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QAwItD"}
@@ -1,13 +1,20 @@
1
1
  import { resolveCredentials } from '../lib/client.js';
2
2
  import { printError, printJson } from '../lib/output.js';
3
+ import { resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC } from '../lib/text-input.js';
3
4
  /**
4
5
  * `parall wechat …` — the per-vendor platform verb for the personal-WeChat
5
6
  * protocol channel (wechatapi.net, tier B in the multi-channel
6
7
  * architecture). The agent sends as the connected WeChat account itself;
7
8
  * the vendor token + device id never reach the runtime — api-server
8
9
  * performs the vendor call in-process.
10
+ *
11
+ * Verb surface = the vendor's in-scope interface aggregated: postText
12
+ * (send), fetchContactsList + getBriefInfo + getChatroomInfo (contacts),
13
+ * getProfile (profile), checkOnline (status). Every verb renders
14
+ * agent-readable text by default and `--json` for machine output.
15
+ *
9
16
  * INTERNAL research preview: the capability is flag-gated to the internal
10
- * org. Design: docs/engineering-design/wechat-channel-design.md §3.
17
+ * org. Design: docs/engineering-design/wechat-channel-design.md §3, §4.
11
18
  */
12
19
  export function registerWechatCommands(program) {
13
20
  const wechat = program
@@ -17,18 +24,29 @@ export function registerWechatCommands(program) {
17
24
  .command('send')
18
25
  .description('Send a message to a WeChat conversation this connection has seen inbound')
19
26
  .requiredOption('--to <wxid>', 'Vendor-native conversation id from the inbound event (a friend wxid, or a room id ending in @chatroom)')
20
- .requiredOption('--text <text>', 'Message text (plain text)')
27
+ .option('--text <text>', TEXT_OPTION_DESC)
28
+ .option('--text-file <path>', TEXT_FILE_OPTION_DESC)
21
29
  .option('--at <wxid>', 'In group replies: @-mention one member (wxid of the person you are answering)')
30
+ .option('--json', 'Emit the full machine-readable response instead of the readable summary')
22
31
  .action(async (opts) => {
23
32
  try {
33
+ const text = resolveMessageText(opts);
34
+ if (text === undefined || text === '') {
35
+ throw new Error('Provide --text or --text-file');
36
+ }
24
37
  const { client, orgId } = resolveCredentials();
25
38
  const sent = await client.sendChannelMessage(orgId, {
26
39
  channel_type: 'wechat',
27
40
  conversation_id: opts.to,
28
- text: opts.text,
41
+ text,
29
42
  ...(opts.at ? { at: opts.at } : {}),
30
43
  });
31
- printJson(sent);
44
+ if (opts.json) {
45
+ printJson(sent);
46
+ return;
47
+ }
48
+ const where = opts.at ? ` @${opts.at}` : '';
49
+ console.log(`sent: ${sent.message_id} → ${sent.conversation_id}${where} (wechat)`);
32
50
  }
33
51
  catch (err) {
34
52
  printError(err);
@@ -36,11 +54,81 @@ export function registerWechatCommands(program) {
36
54
  });
37
55
  wechat
38
56
  .command('contacts')
39
- .description('List the account address book: friend wxids, saved group room ids, followed official accounts (ids only — the vendor returns no names)')
40
- .action(async () => {
57
+ .description('List the account address book with display names: friends, saved groups, followed official accounts')
58
+ .option('--json', 'Emit the full machine-readable response instead of the readable summary')
59
+ .action(async (opts) => {
60
+ try {
61
+ const { client, orgId } = resolveCredentials();
62
+ const page = await client.listWechatContacts(orgId);
63
+ if (opts.json) {
64
+ printJson(page);
65
+ return;
66
+ }
67
+ const friendRows = page.friends.map((f) => `${f.wxid}\t${f.display}`.replace(/\t$/, ''));
68
+ const groupRows = page.chatrooms.map((c) => `${c.chatroom_id}\t${c.display}`.replace(/\t$/, ''));
69
+ console.log(`ADDRESS BOOK — friends: ${page.friends.length}, groups: ${page.chatrooms.length}, official accounts: ${page.ghs.length}`);
70
+ if (friendRows.length) {
71
+ console.log(`\nfriends (${page.friends.length}):\n ${friendRows.join('\n ')}`);
72
+ }
73
+ if (groupRows.length) {
74
+ console.log(`\ngroups (${page.chatrooms.length}):\n ${groupRows.join('\n ')}`);
75
+ }
76
+ if (page.ghs.length) {
77
+ console.log(`\nofficial accounts (${page.ghs.length}):\n ${page.ghs.join('\n ')}`);
78
+ }
79
+ }
80
+ catch (err) {
81
+ printError(err);
82
+ }
83
+ });
84
+ wechat
85
+ .command('profile')
86
+ .description('Show the connected WeChat account identity (wxid / alias / nickName / app id)')
87
+ .option('--json', 'Emit the full machine-readable response instead of the readable summary')
88
+ .action(async (opts) => {
89
+ try {
90
+ const { client, orgId } = resolveCredentials();
91
+ const view = await client.wechatProfile(orgId);
92
+ if (opts.json) {
93
+ printJson(view);
94
+ return;
95
+ }
96
+ console.log('ACCOUNT');
97
+ console.log(` wxid: ${view.wxid}`);
98
+ if (view.nick_name)
99
+ console.log(` nickName: ${view.nick_name}`);
100
+ if (view.alias)
101
+ console.log(` alias: ${view.alias}`);
102
+ console.log(` app_id: ${view.app_id}`);
103
+ }
104
+ catch (err) {
105
+ printError(err);
106
+ }
107
+ });
108
+ wechat
109
+ .command('status')
110
+ .description('Check the connection health: live online probe, identity, last offline time')
111
+ .option('--json', 'Emit the full machine-readable response instead of the readable summary')
112
+ .action(async (opts) => {
41
113
  try {
42
114
  const { client, orgId } = resolveCredentials();
43
- printJson(await client.listWechatContacts(orgId));
115
+ const view = await client.wechatStatus(orgId);
116
+ if (opts.json) {
117
+ printJson(view);
118
+ return;
119
+ }
120
+ const label = view.online ? 'ONLINE' : 'OFFLINE';
121
+ console.log(`WeChat account: ${label}`);
122
+ console.log(` wxid: ${view.wxid}`);
123
+ if (view.nick_name)
124
+ console.log(` nickName: ${view.nick_name}`);
125
+ if (view.alias)
126
+ console.log(` alias: ${view.alias}`);
127
+ console.log(` app_id: ${view.app_id}`);
128
+ console.log(` last_offline_at: ${view.last_offline_at ?? 'never'}`);
129
+ if (!view.online) {
130
+ console.log(' → the account is offline; a human must re-login on the vendor side. Stop and tell the user.');
131
+ }
44
132
  }
45
133
  catch (err) {
46
134
  printError(err);
@@ -0,0 +1,11 @@
1
+ import { type MCPConfigResponse, type MCPConnectionSummary } from '@parall/sdk';
2
+ /**
3
+ * List a clip's MCP connections (default first). Resolves null when the
4
+ * surface is unavailable — older server without the plural family (404 route
5
+ * miss) or cap:clip-mcp off — so callers degrade to the plain connection rows
6
+ * instead of failing discovery.
7
+ */
8
+ export declare function listClipMCPConnectionSummaries(orgId: string, clipId: string): Promise<MCPConnectionSummary[] | null>;
9
+ /** Per-config read — the only surface carrying the full tools snapshot. */
10
+ export declare function getClipMCPConfigById(orgId: string, clipId: string, configId: string): Promise<MCPConfigResponse>;
11
+ //# sourceMappingURL=mcp-connections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-connections.d.ts","sourceRoot":"","sources":["../../src/lib/mcp-connections.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,iBAAiB,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAwC3F;;;;;GAKG;AACH,wBAAsB,8BAA8B,CAClD,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,oBAAoB,EAAE,GAAG,IAAI,CAAC,CAsBxC;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,iBAAiB,CAAC,CAE5B"}
@@ -0,0 +1,68 @@
1
+ import { ENDPOINTS } from '@parall/sdk';
2
+ // Raw reads of the multi-connection MCP config surface. The SDK deliberately
3
+ // exposes only contract types + endpoint builders for this family (the #2281
4
+ // layering — no ParallClient methods), so the CLI carries its own thin fetch:
5
+ // X-Api-Key auth from the same env resolveCredentials() uses, swimlane header
6
+ // included so lane testing behaves like every other CLI call.
7
+ function headers() {
8
+ const h = {
9
+ 'X-Api-Key': process.env.PRLL_API_KEY ?? '',
10
+ 'Content-Type': 'application/json',
11
+ };
12
+ const lane = process.env.PRLL_SWIMLANE_NAME?.trim();
13
+ if (lane)
14
+ h['X-Prll-Swimlane'] = lane;
15
+ return h;
16
+ }
17
+ async function get(path) {
18
+ const base = (process.env.PRLL_API_URL ?? '').replace(/\/$/, '');
19
+ const res = await fetch(`${base}${path}`, { headers: headers() });
20
+ if (!res.ok) {
21
+ const text = await res.text();
22
+ let message = `API ${res.status}: ${text}`;
23
+ let code;
24
+ try {
25
+ const parsed = JSON.parse(text);
26
+ if (typeof parsed.error?.message === 'string')
27
+ message = parsed.error.message;
28
+ if (typeof parsed.error?.code === 'string')
29
+ code = parsed.error.code;
30
+ }
31
+ catch {
32
+ // non-JSON error body — keep the raw text
33
+ }
34
+ const err = new Error(message);
35
+ err.status = res.status;
36
+ err.code = code;
37
+ throw err;
38
+ }
39
+ return res.json();
40
+ }
41
+ /**
42
+ * List a clip's MCP connections (default first). Resolves null when the
43
+ * surface is unavailable — older server without the plural family (404 route
44
+ * miss) or cap:clip-mcp off — so callers degrade to the plain connection rows
45
+ * instead of failing discovery.
46
+ */
47
+ export async function listClipMCPConnectionSummaries(orgId, clipId) {
48
+ try {
49
+ const resp = await get(ENDPOINTS.ORG_CLIP_MCP_CONFIGS(orgId, clipId));
50
+ return resp.data ?? [];
51
+ }
52
+ catch (err) {
53
+ // Degrade to plain connection rows — but leave a diagnosable trace for
54
+ // anything that is NOT the expected "surface unavailable" shape (older
55
+ // server → 404 route miss), so an expired key or network fault doesn't
56
+ // silently read as "no MCP annotations". stderr only: stdout is the JSON
57
+ // contract.
58
+ const status = err.status;
59
+ if (status !== 404) {
60
+ console.error(`warning: MCP connection details unavailable (${err instanceof Error ? err.message : String(err)}) — showing plain connection rows`);
61
+ }
62
+ return null;
63
+ }
64
+ }
65
+ /** Per-config read — the only surface carrying the full tools snapshot. */
66
+ export function getClipMCPConfigById(orgId, clipId, configId) {
67
+ return get(ENDPOINTS.ORG_CLIP_MCP_CONFIG_BY_ID(orgId, clipId, configId));
68
+ }
@@ -27,4 +27,12 @@ export declare function resolveMessageText(opts: {
27
27
  text?: string;
28
28
  textFile?: string;
29
29
  }): string | undefined;
30
+ /**
31
+ * Read the payload of a file-input flag (`--text-file`, `--args-file`) from a
32
+ * path or stdin (`-`). Shared so every such flag keeps the same three
33
+ * guarantees: stdin drains a pipe/heredoc synchronously, an interactive TTY is
34
+ * refused instead of hanging the dispatch, and a read failure surfaces its
35
+ * underlying reason under the flag's own name.
36
+ */
37
+ export declare function readTextFileArg(flag: string, value: string): string;
30
38
  //# sourceMappingURL=text-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/lib/text-input.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,oFACsD,CAAC;AACpF,eAAO,MAAM,qBAAqB,mHAC2E,CAAC;AAC9G,eAAO,MAAM,aAAa,yDAAyD,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CA8BjG"}
1
+ {"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/lib/text-input.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,oFACsD,CAAC;AACpF,eAAO,MAAM,qBAAqB,mHAC2E,CAAC;AAC9G,eAAO,MAAM,aAAa,yDAAyD,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAMjG;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBnE"}
@@ -34,28 +34,34 @@ export function resolveMessageText(opts) {
34
34
  }
35
35
  if (opts.textFile === undefined)
36
36
  return opts.text;
37
- let raw;
38
- if (opts.textFile === '-') {
37
+ return readTextFileArg('--text-file', opts.textFile).replace(/\r?\n$/, '');
38
+ }
39
+ /**
40
+ * Read the payload of a file-input flag (`--text-file`, `--args-file`) from a
41
+ * path or stdin (`-`). Shared so every such flag keeps the same three
42
+ * guarantees: stdin drains a pipe/heredoc synchronously, an interactive TTY is
43
+ * refused instead of hanging the dispatch, and a read failure surfaces its
44
+ * underlying reason under the flag's own name.
45
+ */
46
+ export function readTextFileArg(flag, value) {
47
+ if (value === '-') {
39
48
  // fd 0 = stdin; readFileSync drains a heredoc / pipe synchronously. Guard
40
49
  // the interactive case — reading a TTY would block until EOF (which an
41
50
  // agent never sends), hanging the dispatch until its deadline.
42
51
  if (process.stdin.isTTY) {
43
- throw new Error("--text-file - reads the body from stdin; pipe it in (e.g. a quoted heredoc `<<'EOF'`), don't run it interactively");
52
+ throw new Error(`${flag} - reads from stdin; pipe it in (e.g. a quoted heredoc \`<<'EOF'\`), don't run it interactively`);
44
53
  }
45
- raw = fs.readFileSync(0, 'utf-8');
54
+ return fs.readFileSync(0, 'utf-8');
46
55
  }
47
- else {
48
- const resolved = path.resolve(opts.textFile);
49
- try {
50
- raw = fs.readFileSync(resolved, 'utf-8');
51
- }
52
- catch (err) {
53
- // Surface the underlying reason (missing / no permission / is-a-directory)
54
- // so the agent can self-correct printError renders only `.message` — and
55
- // keep the original error as `cause` for anything that inspects it.
56
- const reason = err instanceof Error ? err.message : String(err);
57
- throw new Error(`Cannot read --text-file ${opts.textFile}: ${reason}`, { cause: err });
58
- }
56
+ const resolved = path.resolve(value);
57
+ try {
58
+ return fs.readFileSync(resolved, 'utf-8');
59
+ }
60
+ catch (err) {
61
+ // Surface the underlying reason (missing / no permission / is-a-directory)
62
+ // so the agent can self-correct printError renders only `.message` — and
63
+ // keep the original error as `cause` for anything that inspects it.
64
+ const reason = err instanceof Error ? err.message : String(err);
65
+ throw new Error(`Cannot read ${flag} ${value}: ${reason}`, { cause: err });
59
66
  }
60
- return raw.replace(/\r?\n$/, '');
61
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.53.0",
3
+ "version": "1.55.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.53.0",
40
- "@parall/sdk": "1.53.0"
39
+ "@parall/agent-core": "1.55.0",
40
+ "@parall/sdk": "1.55.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.53.0"
46
+ "@parall/agent-core": "1.55.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc",