@parall/cli 1.36.1 → 1.37.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":"dm.d.ts","sourceRoot":"","sources":["../../src/commands/dm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiCpC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,QAwDlD"}
1
+ {"version":3,"file":"dm.d.ts","sourceRoot":"","sources":["../../src/commands/dm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuCpC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,QA+DlD"}
@@ -1,5 +1,6 @@
1
1
  import { resolveCredentials, resolveRuntimeContext } from '../lib/client.js';
2
2
  import { printJson, printError, printRefHint, stripPrllScheme } from '../lib/output.js';
3
+ import { NO_BODY_ERROR, resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC, } from '../lib/text-input.js';
3
4
  import { uploadFile } from '../lib/upload.js';
4
5
  /**
5
6
  * Resolve a name-or-id argument to a user ID.
@@ -25,7 +26,8 @@ export function registerDMCommands(program) {
25
26
  .command('dm')
26
27
  .description('Send a direct message to a user by name or ID (auto-creates chat if needed)')
27
28
  .argument('<nameOrId>', 'Target user display name or ID (usr_...)')
28
- .option('--text <text>', 'Message text')
29
+ .option('--text <text>', TEXT_OPTION_DESC)
30
+ .option('--text-file <path>', TEXT_FILE_OPTION_DESC)
29
31
  .option('--file <path>', 'Upload and attach a local file')
30
32
  .option('--attachment <id>', 'Attach an existing attachment (att_xxx or prll://att_xxx)')
31
33
  .option('--no-reply', 'Hint that the recipient should not reply')
@@ -38,7 +40,7 @@ export function registerDMCommands(program) {
38
40
  printError(new Error('--file and --attachment are mutually exclusive'));
39
41
  return;
40
42
  }
41
- const text = opts.text || '';
43
+ const text = resolveMessageText(opts) || '';
42
44
  let attachmentIds;
43
45
  if (opts.file) {
44
46
  const result = await uploadFile(client, orgId, opts.file);
@@ -48,7 +50,7 @@ export function registerDMCommands(program) {
48
50
  attachmentIds = [stripPrllScheme(opts.attachment)];
49
51
  }
50
52
  if (!text && !attachmentIds) {
51
- printError(new Error('Provide --text, --file, or --attachment'));
53
+ printError(new Error(NO_BODY_ERROR));
52
54
  return;
53
55
  }
54
56
  const req = {
@@ -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;AAKzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAwJvD"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QA0JvD"}
@@ -1,6 +1,7 @@
1
1
  import { ApiError } from '@parall/sdk';
2
2
  import { resolveCredentials, resolveRuntimeContext } from '../lib/client.js';
3
3
  import { printError, printJson, printRefHint, stripPrllScheme } from '../lib/output.js';
4
+ import { NO_BODY_ERROR, resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC, } from '../lib/text-input.js';
4
5
  import { uploadFile } from '../lib/upload.js';
5
6
  export function registerMessageCommands(program) {
6
7
  const messages = program.command('messages').description('Manage messages');
@@ -53,7 +54,8 @@ export function registerMessageCommands(program) {
53
54
  .command('send')
54
55
  .description('Send a message to a chat (text, file, or both)')
55
56
  .argument('[chatId]', 'Chat ID (defaults to PRLL_CHAT_ID if set)')
56
- .option('--text <text>', 'Message text')
57
+ .option('--text <text>', TEXT_OPTION_DESC)
58
+ .option('--text-file <path>', TEXT_FILE_OPTION_DESC)
57
59
  .option('--file <path>', 'Upload and attach a local file')
58
60
  .option('--attachment <id>', 'Attach an existing attachment (att_xxx or prll://att_xxx)')
59
61
  .option('--thread-root-id <id>', 'Reply to a thread')
@@ -70,14 +72,14 @@ export function registerMessageCommands(program) {
70
72
  // `messages send` targets a chat. A user id here is an addressing
71
73
  // mistake — point at `dm` instead of letting the server reject it.
72
74
  if (chatId.startsWith('usr_')) {
73
- printError(new ApiError(400, `prll://${chatId} is a user, not a chat. To message a user use: parall dm prll://${chatId} --text "..."`, 'INVALID_TARGET'));
75
+ printError(new ApiError(400, `prll://${chatId} is a user, not a chat. To message a user use: parall dm prll://${chatId} --text-file -`, 'INVALID_TARGET'));
74
76
  return;
75
77
  }
76
78
  if (opts.file && opts.attachment) {
77
79
  printError(new Error('--file and --attachment are mutually exclusive'));
78
80
  return;
79
81
  }
80
- const text = opts.text || '';
82
+ const text = resolveMessageText(opts) || '';
81
83
  let attachmentIds;
82
84
  if (opts.file) {
83
85
  const result = await uploadFile(client, orgId, opts.file);
@@ -87,7 +89,7 @@ export function registerMessageCommands(program) {
87
89
  attachmentIds = [stripPrllScheme(opts.attachment)];
88
90
  }
89
91
  if (!text && !attachmentIds) {
90
- printError(new Error('Provide --text, --file, or --attachment'));
92
+ printError(new Error(NO_BODY_ERROR));
91
93
  return;
92
94
  }
93
95
  const req = {
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAqTpD"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA6UpD"}
@@ -268,10 +268,20 @@ export function registerTaskCommands(program) {
268
268
  .command('watch')
269
269
  .description('Watch a task (subscribe to comment notifications)')
270
270
  .argument('<taskId>', 'Task ID')
271
- .action(async (taskId) => {
271
+ .option('--user-id <userId>', 'Subscribe another user instead of yourself (requires you to be the task creator, assignee, project lead, or an org owner/admin)')
272
+ .action(async (taskId, opts) => {
272
273
  try {
273
274
  const { client, orgId } = resolveCredentials();
274
- await client.watchTask(orgId, stripPrllScheme(taskId));
275
+ const normalizedTaskId = stripPrllScheme(taskId);
276
+ const targetUserId = opts.userId ? stripPrllScheme(opts.userId) : undefined;
277
+ // A self-targeted --user-id is equivalent to plain self-watch; route it
278
+ // back to /watch so the behaviour matches `parall tasks watch` exactly.
279
+ if (targetUserId && targetUserId !== (await client.getMe()).id) {
280
+ await client.subscribeTaskMember(orgId, normalizedTaskId, targetUserId);
281
+ }
282
+ else {
283
+ await client.watchTask(orgId, normalizedTaskId);
284
+ }
275
285
  printJson({ ok: true });
276
286
  }
277
287
  catch (err) {
@@ -282,10 +292,20 @@ export function registerTaskCommands(program) {
282
292
  .command('unwatch')
283
293
  .description('Unwatch a task (stop receiving comment notifications)')
284
294
  .argument('<taskId>', 'Task ID')
285
- .action(async (taskId) => {
295
+ .option('--user-id <userId>', 'Unsubscribe another user instead of yourself (requires you to be the task creator, assignee, project lead, or an org owner/admin)')
296
+ .action(async (taskId, opts) => {
286
297
  try {
287
298
  const { client, orgId } = resolveCredentials();
288
- await client.unwatchTask(orgId, stripPrllScheme(taskId));
299
+ const normalizedTaskId = stripPrllScheme(taskId);
300
+ const targetUserId = opts.userId ? stripPrllScheme(opts.userId) : undefined;
301
+ // A self-targeted --user-id is equivalent to plain self-unwatch; route it
302
+ // back to /watch so the behaviour matches `parall tasks unwatch` exactly.
303
+ if (targetUserId && targetUserId !== (await client.getMe()).id) {
304
+ await client.unsubscribeTaskMember(orgId, normalizedTaskId, targetUserId);
305
+ }
306
+ else {
307
+ await client.unwatchTask(orgId, normalizedTaskId);
308
+ }
289
309
  printJson({ ok: true });
290
310
  }
291
311
  catch (err) {
@@ -0,0 +1,30 @@
1
+ export declare const TEXT_OPTION_DESC = "Message text (shell-safe only for short literals with no $, backtick, or quote)";
2
+ export declare const TEXT_FILE_OPTION_DESC = "Read message text from a file ('-' = stdin) \u2014 shell-safe channel for content with $, backticks, or quotes";
3
+ export declare const NO_BODY_ERROR = "Provide --text, --text-file, --file, or --attachment";
4
+ /**
5
+ * Resolve message body text from the mutually-exclusive `--text` / `--text-file`
6
+ * options shared by `messages send` and `dm`.
7
+ *
8
+ * Agents build these commands with an LLM and run them through a shell, so any
9
+ * body passed as a double-quoted `--text "..."` argument is mangled by shell
10
+ * expansion *before* the CLI ever sees it: `$1,000` becomes `,000`, `$USER`
11
+ * expands, and `` `cmd` `` / `$(cmd)` execute. Single quotes are no better —
12
+ * they break on the apostrophes that fill natural-language replies. `--text-file`
13
+ * sidesteps the shell entirely: the body comes from a file the agent wrote (raw
14
+ * bytes, no shell) or from stdin via a quoted heredoc (`<<'EOF'`, which disables
15
+ * all expansion). `-` means stdin.
16
+ *
17
+ * A single trailing newline — the structural newline a heredoc or editor appends
18
+ * — is stripped so a one-line reply doesn't arrive with a dangling blank line;
19
+ * this mirrors how `$(...)` command substitution trims trailing newlines, and
20
+ * leading/interior whitespace is preserved untouched.
21
+ *
22
+ * Returns `undefined` when neither option is set (preserving the old
23
+ * `opts.text`-or-nothing contract). Throws on conflicting options or an
24
+ * unreadable file; callers already funnel that into `printError`.
25
+ */
26
+ export declare function resolveMessageText(opts: {
27
+ text?: string;
28
+ textFile?: string;
29
+ }): string | undefined;
30
+ //# sourceMappingURL=text-input.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,61 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ // Shared option help + validation copy for the message-body flags, so
4
+ // `messages send` and `dm` stay in lockstep. commander's --help prints these
5
+ // verbatim (no markdown), so keep them backtick-free.
6
+ export const TEXT_OPTION_DESC = 'Message text (shell-safe only for short literals with no $, backtick, or quote)';
7
+ export const TEXT_FILE_OPTION_DESC = "Read message text from a file ('-' = stdin) — shell-safe channel for content with $, backticks, or quotes";
8
+ export const NO_BODY_ERROR = 'Provide --text, --text-file, --file, or --attachment';
9
+ /**
10
+ * Resolve message body text from the mutually-exclusive `--text` / `--text-file`
11
+ * options shared by `messages send` and `dm`.
12
+ *
13
+ * Agents build these commands with an LLM and run them through a shell, so any
14
+ * body passed as a double-quoted `--text "..."` argument is mangled by shell
15
+ * expansion *before* the CLI ever sees it: `$1,000` becomes `,000`, `$USER`
16
+ * expands, and `` `cmd` `` / `$(cmd)` execute. Single quotes are no better —
17
+ * they break on the apostrophes that fill natural-language replies. `--text-file`
18
+ * sidesteps the shell entirely: the body comes from a file the agent wrote (raw
19
+ * bytes, no shell) or from stdin via a quoted heredoc (`<<'EOF'`, which disables
20
+ * all expansion). `-` means stdin.
21
+ *
22
+ * A single trailing newline — the structural newline a heredoc or editor appends
23
+ * — is stripped so a one-line reply doesn't arrive with a dangling blank line;
24
+ * this mirrors how `$(...)` command substitution trims trailing newlines, and
25
+ * leading/interior whitespace is preserved untouched.
26
+ *
27
+ * Returns `undefined` when neither option is set (preserving the old
28
+ * `opts.text`-or-nothing contract). Throws on conflicting options or an
29
+ * unreadable file; callers already funnel that into `printError`.
30
+ */
31
+ export function resolveMessageText(opts) {
32
+ if (opts.text !== undefined && opts.textFile !== undefined) {
33
+ throw new Error('--text and --text-file are mutually exclusive');
34
+ }
35
+ if (opts.textFile === undefined)
36
+ return opts.text;
37
+ let raw;
38
+ if (opts.textFile === '-') {
39
+ // fd 0 = stdin; readFileSync drains a heredoc / pipe synchronously. Guard
40
+ // the interactive case — reading a TTY would block until EOF (which an
41
+ // agent never sends), hanging the dispatch until its deadline.
42
+ 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");
44
+ }
45
+ raw = fs.readFileSync(0, 'utf-8');
46
+ }
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
+ }
59
+ }
60
+ return raw.replace(/\r?\n$/, '');
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.36.1",
3
+ "version": "1.37.0",
4
4
  "description": "CLI client for Parall — universal agent & human access to Parall API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,13 +36,13 @@
36
36
  "diff": "^8.0.3",
37
37
  "js-yaml": "^4.1.0",
38
38
  "zod": "^4.3.6",
39
- "@parall/sdk": "1.36.1"
39
+ "@parall/sdk": "1.37.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/js-yaml": "^4.0.9",
43
43
  "@types/node": "^22.0.0",
44
44
  "typescript": "^5.7.0",
45
- "@parall/agent-core": "1.36.1"
45
+ "@parall/agent-core": "1.37.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc",