@parall/cli 1.15.0 → 1.16.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;AA4BpC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,QA6BlD"}
1
+ {"version":3,"file":"dm.d.ts","sourceRoot":"","sources":["../../src/commands/dm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,QAkDlD"}
@@ -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 { uploadFile } from '../lib/upload.js';
3
4
  /**
4
5
  * Resolve a name-or-id argument to a user ID.
5
6
  * If it starts with "usr_", treat as ID. Otherwise, search org members by display_name.
@@ -24,22 +25,41 @@ export function registerDMCommands(program) {
24
25
  .command('dm')
25
26
  .description('Send a direct message to a user by name or ID (auto-creates chat if needed)')
26
27
  .argument('<nameOrId>', 'Target user display name or ID (usr_...)')
27
- .requiredOption('--text <text>', 'Message text')
28
+ .option('--text <text>', 'Message text')
29
+ .option('--file <path>', 'Upload and attach a local file')
30
+ .option('--attachment <id>', 'Attach an existing attachment (att_xxx or prll://att_xxx)')
28
31
  .option('--no-reply', 'Hint that the recipient should not reply')
29
32
  .action(async (nameOrId, opts) => {
30
33
  try {
31
34
  const { client, orgId } = resolveCredentials();
32
35
  const ctx = resolveRuntimeContext();
33
36
  const userId = await resolveUserId(client, orgId, nameOrId);
37
+ if (opts.file && opts.attachment) {
38
+ printError(new Error('--file and --attachment are mutually exclusive'));
39
+ return;
40
+ }
41
+ const text = opts.text || '';
42
+ let attachmentIds;
43
+ if (opts.file) {
44
+ const result = await uploadFile(client, orgId, opts.file);
45
+ attachmentIds = [result.attachmentId];
46
+ }
47
+ else if (opts.attachment) {
48
+ attachmentIds = [stripPrllScheme(opts.attachment)];
49
+ }
50
+ if (!text && !attachmentIds) {
51
+ printError(new Error('Provide --text, --file, or --attachment'));
52
+ return;
53
+ }
34
54
  const req = {
35
55
  user_id: userId,
36
56
  message_type: 'text',
37
- content: { text: opts.text },
57
+ content: { text },
38
58
  };
39
- // Commander parses --no-reply as opts.reply = false
40
- if (opts.reply === false) {
59
+ if (attachmentIds)
60
+ req.attachment_ids = attachmentIds;
61
+ if (opts.reply === false)
41
62
  req.hints = { no_reply: true };
42
- }
43
63
  if (ctx.stepId)
44
64
  req.agent_step_id = ctx.stepId;
45
65
  const result = await client.sendDirectMessage(orgId, req);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerFileCommands(program: Command): void;
3
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/commands/files.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAoEpD"}
@@ -0,0 +1,66 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { resolveCredentials } from '../lib/client.js';
4
+ import { printJson, printError, stripPrllScheme } from '../lib/output.js';
5
+ import { uploadFile } from '../lib/upload.js';
6
+ export function registerFileCommands(program) {
7
+ const files = program.command('files').description('Upload and download files');
8
+ files
9
+ .command('upload')
10
+ .description('Upload a file and return attachment metadata')
11
+ .argument('<path>', 'Local file path to upload')
12
+ .action(async (filePath) => {
13
+ try {
14
+ const { client, orgId } = resolveCredentials();
15
+ const result = await uploadFile(client, orgId, filePath);
16
+ printJson({
17
+ attachment_id: result.attachmentId,
18
+ file_name: result.fileName,
19
+ file_size: result.fileSize,
20
+ mime_type: result.mimeType,
21
+ });
22
+ }
23
+ catch (err) {
24
+ printError(err);
25
+ }
26
+ });
27
+ files
28
+ .command('download')
29
+ .description('Download a file by attachment ID')
30
+ .argument('<attachmentId>', 'Attachment ID (att_xxx or prll://att_xxx)')
31
+ .option('-o, --output <path>', 'Output file path (default: ./{original_filename})')
32
+ .action(async (attachmentId, opts) => {
33
+ try {
34
+ const { client } = resolveCredentials();
35
+ const id = stripPrllScheme(attachmentId);
36
+ const fileInfo = await client.getFileUrl(id);
37
+ // Download the file
38
+ const res = await fetch(fileInfo.url);
39
+ if (!res.ok) {
40
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
41
+ }
42
+ const buffer = Buffer.from(await res.arrayBuffer());
43
+ // Sanitize server-provided filename to prevent path traversal
44
+ const safeName = path.basename(fileInfo.file_name || id);
45
+ // Determine output path
46
+ const outputPath = opts.output
47
+ ? path.resolve(opts.output)
48
+ : path.resolve(safeName);
49
+ // If output is a directory, write file inside it
50
+ let finalPath = outputPath;
51
+ if (fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) {
52
+ finalPath = path.join(outputPath, safeName);
53
+ }
54
+ fs.writeFileSync(finalPath, buffer);
55
+ printJson({
56
+ path: finalPath,
57
+ file_name: fileInfo.file_name,
58
+ file_size: fileInfo.file_size,
59
+ mime_type: fileInfo.mime_type,
60
+ });
61
+ }
62
+ catch (err) {
63
+ printError(err);
64
+ }
65
+ });
66
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAkGvD"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAyHvD"}
@@ -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 { uploadFile } from '../lib/upload.js';
3
4
  export function registerMessageCommands(program) {
4
5
  const messages = program.command('messages').description('Manage messages');
5
6
  messages
@@ -46,9 +47,11 @@ export function registerMessageCommands(program) {
46
47
  });
47
48
  messages
48
49
  .command('send')
49
- .description('Send a text message to a chat')
50
+ .description('Send a message to a chat (text, file, or both)')
50
51
  .argument('[chatId]', 'Chat ID (defaults to PRLL_CHAT_ID if set)')
51
- .requiredOption('--text <text>', 'Message text')
52
+ .option('--text <text>', 'Message text')
53
+ .option('--file <path>', 'Upload and attach a local file')
54
+ .option('--attachment <id>', 'Attach an existing attachment (att_xxx or prll://att_xxx)')
52
55
  .option('--thread-root-id <id>', 'Reply to a thread')
53
56
  .option('--no-reply', 'Hint that the recipient should not reply')
54
57
  .action(async (chatIdArg, opts) => {
@@ -60,10 +63,29 @@ export function registerMessageCommands(program) {
60
63
  printError(new Error('Chat ID required — provide as argument or set PRLL_CHAT_ID'));
61
64
  return;
62
65
  }
66
+ if (opts.file && opts.attachment) {
67
+ printError(new Error('--file and --attachment are mutually exclusive'));
68
+ return;
69
+ }
70
+ const text = opts.text || '';
71
+ let attachmentIds;
72
+ if (opts.file) {
73
+ const result = await uploadFile(client, orgId, opts.file);
74
+ attachmentIds = [result.attachmentId];
75
+ }
76
+ else if (opts.attachment) {
77
+ attachmentIds = [stripPrllScheme(opts.attachment)];
78
+ }
79
+ if (!text && !attachmentIds) {
80
+ printError(new Error('Provide --text, --file, or --attachment'));
81
+ return;
82
+ }
63
83
  const req = {
64
84
  message_type: 'text',
65
- content: { text: opts.text },
85
+ content: { text },
66
86
  };
87
+ if (attachmentIds)
88
+ req.attachment_ids = attachmentIds;
67
89
  if (opts.threadRootId !== undefined)
68
90
  req.thread_root_id = stripPrllScheme(opts.threadRootId);
69
91
  if (opts.reply === false)
@@ -0,0 +1,19 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * `parall no-reply` — explicit "I am not replying this turn" signal.
4
+ *
5
+ * Runs entirely client-side: no API call, no server state. The agent bridge
6
+ * (agent-core) sniffs the invocation in the Bash tool_call stream and
7
+ * drops all subsequent text projections for this dispatch, so the agent
8
+ * can silence a turn without leaking a polite "No response needed" message
9
+ * into the chat.
10
+ *
11
+ * Usage pattern in the agent's workspace CLAUDE.md:
12
+ *
13
+ * When a dispatched event has `[Hint: no_reply]` or you decide the turn
14
+ * does not need a visible reply, run this command BEFORE emitting any
15
+ * plain text — anything you say after it is dropped, but anything before
16
+ * is already projected as a chat message.
17
+ */
18
+ export declare function registerNoReplyCommands(program: Command): void;
19
+ //# sourceMappingURL=no-reply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-reply.d.ts","sourceRoot":"","sources":["../../src/commands/no-reply.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAevD"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `parall no-reply` — explicit "I am not replying this turn" signal.
3
+ *
4
+ * Runs entirely client-side: no API call, no server state. The agent bridge
5
+ * (agent-core) sniffs the invocation in the Bash tool_call stream and
6
+ * drops all subsequent text projections for this dispatch, so the agent
7
+ * can silence a turn without leaking a polite "No response needed" message
8
+ * into the chat.
9
+ *
10
+ * Usage pattern in the agent's workspace CLAUDE.md:
11
+ *
12
+ * When a dispatched event has `[Hint: no_reply]` or you decide the turn
13
+ * does not need a visible reply, run this command BEFORE emitting any
14
+ * plain text — anything you say after it is dropped, but anything before
15
+ * is already projected as a chat message.
16
+ */
17
+ export function registerNoReplyCommands(program) {
18
+ program
19
+ .command('no-reply')
20
+ .description('Signal to the bridge that this dispatch turn should produce no visible reply. ' +
21
+ 'Subsequent text output is dropped.')
22
+ .option('--reason <reason>', 'Optional human-readable reason, logged for debugging')
23
+ .action((opts) => {
24
+ // Print a deterministic marker so the bridge (and operators reading
25
+ // tool_use logs) can confirm the suppression was requested.
26
+ const payload = { ack: 'no-reply' };
27
+ if (opts.reason)
28
+ payload.reason = opts.reason;
29
+ process.stdout.write(JSON.stringify(payload) + '\n');
30
+ });
31
+ }
package/dist/index.js CHANGED
@@ -10,7 +10,9 @@ import { registerProjectCommands } from './commands/projects.js';
10
10
  import { registerUserCommands } from './commands/users.js';
11
11
  import { registerWikiCommands } from './commands/wiki.js';
12
12
  import { registerMcpCommands } from './commands/mcp.js';
13
+ import { registerNoReplyCommands } from './commands/no-reply.js';
13
14
  import { registerRefCommands } from './commands/refs.js';
15
+ import { registerFileCommands } from './commands/files.js';
14
16
  const require = createRequire(import.meta.url);
15
17
  const pkg = require('../package.json');
16
18
  const program = new Command();
@@ -27,5 +29,7 @@ registerProjectCommands(program);
27
29
  registerUserCommands(program);
28
30
  registerWikiCommands(program);
29
31
  registerMcpCommands(program);
32
+ registerNoReplyCommands(program);
30
33
  registerRefCommands(program);
34
+ registerFileCommands(program);
31
35
  program.parse();
@@ -0,0 +1,9 @@
1
+ import type { ParallClient } from '@parall/sdk';
2
+ export declare function inferMimeType(filePath: string): string;
3
+ export declare function uploadFile(client: ParallClient, orgId: string, filePath: string): Promise<{
4
+ attachmentId: string;
5
+ fileName: string;
6
+ fileSize: number;
7
+ mimeType: string;
8
+ }>;
9
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/lib/upload.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB/K"}
@@ -0,0 +1,38 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ const MIME_MAP = {
4
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
5
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.pdf': 'application/pdf',
6
+ '.json': 'application/json', '.txt': 'text/plain', '.md': 'text/markdown',
7
+ '.csv': 'text/csv', '.zip': 'application/zip', '.tar': 'application/x-tar',
8
+ '.gz': 'application/gzip', '.mp4': 'video/mp4', '.webm': 'video/webm',
9
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.html': 'text/html',
10
+ '.js': 'text/javascript', '.ts': 'text/typescript', '.go': 'text/x-go',
11
+ '.py': 'text/x-python', '.rs': 'text/x-rust',
12
+ };
13
+ export function inferMimeType(filePath) {
14
+ const ext = path.extname(filePath).toLowerCase();
15
+ return MIME_MAP[ext] || 'application/octet-stream';
16
+ }
17
+ export async function uploadFile(client, orgId, filePath) {
18
+ const resolved = path.resolve(filePath);
19
+ const stat = fs.statSync(resolved);
20
+ const fileName = path.basename(resolved);
21
+ const mimeType = inferMimeType(fileName);
22
+ const presign = await client.getUploadPresignUrl(orgId, {
23
+ file_name: fileName,
24
+ file_size: stat.size,
25
+ mime_type: mimeType,
26
+ });
27
+ const fileBuffer = fs.readFileSync(resolved);
28
+ const uploadRes = await fetch(presign.upload_url, {
29
+ method: 'PUT',
30
+ body: fileBuffer,
31
+ headers: { 'Content-Type': mimeType },
32
+ });
33
+ if (!uploadRes.ok) {
34
+ throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
35
+ }
36
+ await client.completeUpload(orgId, presign.attachment_id);
37
+ return { attachmentId: presign.attachment_id, fileName, fileSize: stat.size, mimeType };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "CLI client for Parall — universal agent & human access to Parall API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "@modelcontextprotocol/sdk": "1.27.1",
31
31
  "commander": "^13.0.0",
32
32
  "zod": "^4.3.6",
33
- "@parall/sdk": "1.15.0"
33
+ "@parall/sdk": "1.16.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.0.0",