@parall/claude-agent 1.15.1 → 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":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAgBF,qBAAa,iBAAkB,YAAW,eAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,wBAAwB;IAEpD,QAAQ,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA2FjG,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,SAAS;CAkClB"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAiDF,qBAAa,iBAAkB,YAAW,eAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,wBAAwB;IAEpD,QAAQ,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAwGjG,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,SAAS;CAkClB"}
package/dist/dispatch.js CHANGED
@@ -7,14 +7,53 @@ function extractBashCommand(input) {
7
7
  const command = input.command;
8
8
  return typeof command === "string" && command.trim() ? command.trim() : undefined;
9
9
  }
10
+ // Returns the non-flag tokens that follow a Parall CLI invocation in `command`,
11
+ // or null if the command isn't a Parall CLI call. Handles all the documented
12
+ // forms — bare `parall`, `npx [--yes|-y] @parall/cli[@version]`,
13
+ // `pnpm (exec|dlx) parall` — and skips `npx` flags so that `npx --yes`
14
+ // matches as well as `npx -y`. Tokenization avoids the regex false-match
15
+ // where `messages send --no-reply` looked like the receiver-side `no-reply`.
16
+ function parseParallCliInvocation(command) {
17
+ const tokens = command.replace(/\s+/g, " ").trim().split(" ");
18
+ let i = 0;
19
+ if (tokens[i] === "parall") {
20
+ i++;
21
+ }
22
+ else if (tokens[i] === "npx") {
23
+ i++;
24
+ while (i < tokens.length && tokens[i].startsWith("-"))
25
+ i++;
26
+ if (i >= tokens.length || !/^@parall\/cli(?:@.+)?$/.test(tokens[i]))
27
+ return null;
28
+ i++;
29
+ }
30
+ else if (tokens[i] === "pnpm") {
31
+ i++;
32
+ if (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx"))
33
+ i++;
34
+ if (i >= tokens.length || tokens[i] !== "parall")
35
+ return null;
36
+ i++;
37
+ }
38
+ else {
39
+ return null;
40
+ }
41
+ return tokens.slice(i).filter((t) => !t.startsWith("-"));
42
+ }
10
43
  function isParallSendCommand(command) {
11
44
  if (!command)
12
45
  return false;
13
- const normalized = command.replace(/\s+/g, " ").trim();
14
- const invokesParallCli = /(?:^| )(?:parall|npx @parall\/cli(?:@latest)?|pnpm (?:exec|dlx) parall)(?: |$)/.test(normalized);
15
- if (!invokesParallCli)
46
+ const sub = parseParallCliInvocation(command);
47
+ if (!sub || sub.length === 0)
48
+ return false;
49
+ // `dm <user>` or `messages send <chat>`
50
+ return sub[0] === "dm" || (sub[0] === "messages" && sub[1] === "send");
51
+ }
52
+ function isParallNoReplyCommand(command) {
53
+ if (!command)
16
54
  return false;
17
- return /\b(messages send|dm)\b/.test(normalized);
55
+ const sub = parseParallCliInvocation(command);
56
+ return sub?.[0] === "no-reply";
18
57
  }
19
58
  export class ClaudeCodeAdapter {
20
59
  opts;
@@ -55,6 +94,10 @@ export class ClaudeCodeAdapter {
55
94
  const groupKey = randomUUID();
56
95
  let sawError = false;
57
96
  let suppressProjectedText = false;
97
+ // Sticky for the whole dispatch: set when the agent explicitly calls
98
+ // `parall no-reply` to silence the turn. All subsequent text events are
99
+ // emitted with project=false so the bridge never leaks a chat message.
100
+ let noReplyCalled = false;
58
101
  let completed = false;
59
102
  try {
60
103
  for await (const event of parseClaudeStreamJson(proc.stdout)) {
@@ -62,8 +105,14 @@ export class ClaudeCodeAdapter {
62
105
  this.opts.sessionManager.recordSessionId(sessionKey, event.sessionId);
63
106
  continue;
64
107
  }
65
- if (event.type === "tool_call" && isParallSendCommand(extractBashCommand(event.input))) {
66
- suppressProjectedText = true;
108
+ if (event.type === "tool_call") {
109
+ const cmd = extractBashCommand(event.input);
110
+ if (isParallNoReplyCommand(cmd)) {
111
+ noReplyCalled = true;
112
+ }
113
+ else if (isParallSendCommand(cmd)) {
114
+ suppressProjectedText = true;
115
+ }
67
116
  }
68
117
  if (event.type === "error") {
69
118
  sawError = true;
@@ -71,7 +120,11 @@ export class ClaudeCodeAdapter {
71
120
  continue;
72
121
  }
73
122
  if (event.type === "text") {
74
- const project = suppressProjectedText ? false : event.project;
123
+ const project = noReplyCalled
124
+ ? false
125
+ : suppressProjectedText
126
+ ? false
127
+ : event.project;
75
128
  suppressProjectedText = false;
76
129
  yield {
77
130
  ...event,
package/dist/index.js CHANGED
@@ -15,8 +15,8 @@ function createLogger(prefix) {
15
15
  }
16
16
  async function main() {
17
17
  const config = resolveClaudeAgentConfig(process.env);
18
- ensureClaudeWorkspace(config.workspaceDir);
19
18
  const log = createLogger("claude-agent");
19
+ ensureClaudeWorkspace(config.workspaceDir, log);
20
20
  const client = new ParallClient({
21
21
  baseUrl: config.apiUrl,
22
22
  token: config.apiKey,
@@ -1,2 +1,4 @@
1
- export declare function ensureClaudeWorkspace(workspaceDir: string): void;
1
+ export declare function ensureClaudeWorkspace(workspaceDir: string, log?: {
2
+ warn: (msg: string) => void;
3
+ }): void;
2
4
  //# sourceMappingURL=workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,QAQzD"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAiCA,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,QA0BtC"}
package/dist/workspace.js CHANGED
@@ -13,24 +13,43 @@ Reply with plain text when you want Parall to project your response back into th
13
13
 
14
14
  Use CLI only when you need an explicit side effect instead of plain-text projection:
15
15
 
16
- - \`npx @parall/cli@latest messages send CHT_ID --text "..." \`
17
- - \`npx @parall/cli@latest tasks update TSK_ID --status in_progress\`
18
- - \`npx @parall/cli@latest dm USER_ID --text "..." --no-reply\`
16
+ - \`npx --yes @parall/cli@latest messages send CHT_ID --text "..." \`
17
+ - \`npx --yes @parall/cli@latest tasks update TSK_ID --status in_progress\`
18
+ - \`npx --yes @parall/cli@latest dm USER_ID --text "..." --no-reply\`
19
+ - \`npx --yes @parall/cli@latest no-reply [--reason "..."]\` — end this turn without any visible reply
19
20
 
20
21
  Credentials are already injected through environment variables:
21
22
  \`PRLL_API_URL\`, \`PRLL_API_KEY\`, \`PRLL_ORG_ID\`, \`PRLL_SESSION_ID\`, \`PRLL_CHAT_ID\`, \`PRLL_TRIGGER_MESSAGE_ID\`, \`PRLL_STEP_ID_FILE\`.
22
23
 
23
24
  ## Guardrails
24
25
 
25
- - If an event includes \`[Hint: no_reply]\`, do not send a reply.
26
+ - If an event includes \`[Hint: no_reply]\`, **run \`npx --yes @parall/cli@latest no-reply\` as your first action** and do not type anything — not even "No response needed" / "Noted" / "OK". Plain text becomes a projected chat message; only the \`no-reply\` command silences the turn.
27
+ - Text emitted BEFORE \`no-reply\` in the same turn is already projected as a message and cannot be retracted; text AFTER is dropped by the bridge.
28
+ - A single dispatch may carry several events coalesced together. Once you call \`no-reply\` in a dispatch, **plain text suppression is sticky for the whole turn** — if some events still need a reply, send them explicitly via \`messages send\` / \`dm\`. Plain-text replies after \`no-reply\` are dropped silently.
26
29
  - Prefer plain text over CLI for normal chat replies.
27
30
  - Keep replies concise and task-focused.
28
31
  `;
29
- export function ensureClaudeWorkspace(workspaceDir) {
32
+ export function ensureClaudeWorkspace(workspaceDir, log) {
30
33
  fs.mkdirSync(workspaceDir, { recursive: true });
31
34
  fs.mkdirSync(path.join(workspaceDir, ".claude"), { recursive: true });
35
+ // CLAUDE.md is bridge-managed: always overwrite so a hosted PVC keeps the
36
+ // current guardrails (e.g. the `no-reply` instruction). Operators must not
37
+ // hand-edit this file — workspace customizations should go into AGENTS.md
38
+ // / SOUL.md / TOOLS.md, which are env-driven and untouched here. Surface a
39
+ // warning when we replace a file whose content diverges, so an operator
40
+ // who did edit it locally gets a signal instead of silently losing changes.
32
41
  const claudeMdPath = path.join(workspaceDir, "CLAUDE.md");
33
- if (!fs.existsSync(claudeMdPath)) {
34
- fs.writeFileSync(claudeMdPath, CLAUDE_MD, "utf8");
42
+ if (log) {
43
+ try {
44
+ const existing = fs.readFileSync(claudeMdPath, "utf8");
45
+ if (existing !== CLAUDE_MD) {
46
+ log.warn(`claude-agent: overwriting divergent ${claudeMdPath} with bridge-managed template ` +
47
+ `(local edits to CLAUDE.md are not preserved — customize AGENTS.md / SOUL.md / TOOLS.md instead)`);
48
+ }
49
+ }
50
+ catch {
51
+ // file missing or unreadable — first-boot case, no warning needed
52
+ }
35
53
  }
54
+ fs.writeFileSync(claudeMdPath, CLAUDE_MD, "utf8");
36
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "Claude Code bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "src"
26
26
  ],
27
27
  "dependencies": {
28
- "@parall/agent-core": "1.15.1",
29
- "@parall/cli": "1.15.1",
30
- "@parall/sdk": "1.15.1"
28
+ "@parall/agent-core": "1.16.0",
29
+ "@parall/cli": "1.16.0",
30
+ "@parall/sdk": "1.16.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -32,12 +32,45 @@ function extractBashCommand(input: unknown): string | undefined {
32
32
  return typeof command === "string" && command.trim() ? command.trim() : undefined;
33
33
  }
34
34
 
35
+ // Returns the non-flag tokens that follow a Parall CLI invocation in `command`,
36
+ // or null if the command isn't a Parall CLI call. Handles all the documented
37
+ // forms — bare `parall`, `npx [--yes|-y] @parall/cli[@version]`,
38
+ // `pnpm (exec|dlx) parall` — and skips `npx` flags so that `npx --yes`
39
+ // matches as well as `npx -y`. Tokenization avoids the regex false-match
40
+ // where `messages send --no-reply` looked like the receiver-side `no-reply`.
41
+ function parseParallCliInvocation(command: string): string[] | null {
42
+ const tokens = command.replace(/\s+/g, " ").trim().split(" ");
43
+ let i = 0;
44
+ if (tokens[i] === "parall") {
45
+ i++;
46
+ } else if (tokens[i] === "npx") {
47
+ i++;
48
+ while (i < tokens.length && tokens[i].startsWith("-")) i++;
49
+ if (i >= tokens.length || !/^@parall\/cli(?:@.+)?$/.test(tokens[i])) return null;
50
+ i++;
51
+ } else if (tokens[i] === "pnpm") {
52
+ i++;
53
+ if (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx")) i++;
54
+ if (i >= tokens.length || tokens[i] !== "parall") return null;
55
+ i++;
56
+ } else {
57
+ return null;
58
+ }
59
+ return tokens.slice(i).filter((t) => !t.startsWith("-"));
60
+ }
61
+
35
62
  function isParallSendCommand(command: string | undefined): boolean {
36
63
  if (!command) return false;
37
- const normalized = command.replace(/\s+/g, " ").trim();
38
- const invokesParallCli = /(?:^| )(?:parall|npx @parall\/cli(?:@latest)?|pnpm (?:exec|dlx) parall)(?: |$)/.test(normalized);
39
- if (!invokesParallCli) return false;
40
- return /\b(messages send|dm)\b/.test(normalized);
64
+ const sub = parseParallCliInvocation(command);
65
+ if (!sub || sub.length === 0) return false;
66
+ // `dm <user>` or `messages send <chat>`
67
+ return sub[0] === "dm" || (sub[0] === "messages" && sub[1] === "send");
68
+ }
69
+
70
+ function isParallNoReplyCommand(command: string | undefined): boolean {
71
+ if (!command) return false;
72
+ const sub = parseParallCliInvocation(command);
73
+ return sub?.[0] === "no-reply";
41
74
  }
42
75
 
43
76
  export class ClaudeCodeAdapter implements DispatchAdapter {
@@ -83,6 +116,10 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
83
116
  const groupKey = randomUUID();
84
117
  let sawError = false;
85
118
  let suppressProjectedText = false;
119
+ // Sticky for the whole dispatch: set when the agent explicitly calls
120
+ // `parall no-reply` to silence the turn. All subsequent text events are
121
+ // emitted with project=false so the bridge never leaks a chat message.
122
+ let noReplyCalled = false;
86
123
  let completed = false;
87
124
 
88
125
  try {
@@ -92,8 +129,13 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
92
129
  continue;
93
130
  }
94
131
 
95
- if (event.type === "tool_call" && isParallSendCommand(extractBashCommand(event.input))) {
96
- suppressProjectedText = true;
132
+ if (event.type === "tool_call") {
133
+ const cmd = extractBashCommand(event.input);
134
+ if (isParallNoReplyCommand(cmd)) {
135
+ noReplyCalled = true;
136
+ } else if (isParallSendCommand(cmd)) {
137
+ suppressProjectedText = true;
138
+ }
97
139
  }
98
140
 
99
141
  if (event.type === "error") {
@@ -103,7 +145,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
103
145
  }
104
146
 
105
147
  if (event.type === "text") {
106
- const project = suppressProjectedText ? false : event.project;
148
+ const project = noReplyCalled
149
+ ? false
150
+ : suppressProjectedText
151
+ ? false
152
+ : event.project;
107
153
  suppressProjectedText = false;
108
154
  yield {
109
155
  ...event,
package/src/index.ts CHANGED
@@ -24,8 +24,8 @@ function createLogger(prefix: string) {
24
24
 
25
25
  async function main() {
26
26
  const config = resolveClaudeAgentConfig(process.env);
27
- ensureClaudeWorkspace(config.workspaceDir);
28
27
  const log = createLogger("claude-agent");
28
+ ensureClaudeWorkspace(config.workspaceDir, log);
29
29
 
30
30
  const client = new ParallClient({
31
31
  baseUrl: config.apiUrl,
package/src/workspace.ts CHANGED
@@ -14,26 +14,49 @@ Reply with plain text when you want Parall to project your response back into th
14
14
 
15
15
  Use CLI only when you need an explicit side effect instead of plain-text projection:
16
16
 
17
- - \`npx @parall/cli@latest messages send CHT_ID --text "..." \`
18
- - \`npx @parall/cli@latest tasks update TSK_ID --status in_progress\`
19
- - \`npx @parall/cli@latest dm USER_ID --text "..." --no-reply\`
17
+ - \`npx --yes @parall/cli@latest messages send CHT_ID --text "..." \`
18
+ - \`npx --yes @parall/cli@latest tasks update TSK_ID --status in_progress\`
19
+ - \`npx --yes @parall/cli@latest dm USER_ID --text "..." --no-reply\`
20
+ - \`npx --yes @parall/cli@latest no-reply [--reason "..."]\` — end this turn without any visible reply
20
21
 
21
22
  Credentials are already injected through environment variables:
22
23
  \`PRLL_API_URL\`, \`PRLL_API_KEY\`, \`PRLL_ORG_ID\`, \`PRLL_SESSION_ID\`, \`PRLL_CHAT_ID\`, \`PRLL_TRIGGER_MESSAGE_ID\`, \`PRLL_STEP_ID_FILE\`.
23
24
 
24
25
  ## Guardrails
25
26
 
26
- - If an event includes \`[Hint: no_reply]\`, do not send a reply.
27
+ - If an event includes \`[Hint: no_reply]\`, **run \`npx --yes @parall/cli@latest no-reply\` as your first action** and do not type anything — not even "No response needed" / "Noted" / "OK". Plain text becomes a projected chat message; only the \`no-reply\` command silences the turn.
28
+ - Text emitted BEFORE \`no-reply\` in the same turn is already projected as a message and cannot be retracted; text AFTER is dropped by the bridge.
29
+ - A single dispatch may carry several events coalesced together. Once you call \`no-reply\` in a dispatch, **plain text suppression is sticky for the whole turn** — if some events still need a reply, send them explicitly via \`messages send\` / \`dm\`. Plain-text replies after \`no-reply\` are dropped silently.
27
30
  - Prefer plain text over CLI for normal chat replies.
28
31
  - Keep replies concise and task-focused.
29
32
  `;
30
33
 
31
- export function ensureClaudeWorkspace(workspaceDir: string) {
34
+ export function ensureClaudeWorkspace(
35
+ workspaceDir: string,
36
+ log?: { warn: (msg: string) => void },
37
+ ) {
32
38
  fs.mkdirSync(workspaceDir, { recursive: true });
33
39
  fs.mkdirSync(path.join(workspaceDir, ".claude"), { recursive: true });
34
40
 
41
+ // CLAUDE.md is bridge-managed: always overwrite so a hosted PVC keeps the
42
+ // current guardrails (e.g. the `no-reply` instruction). Operators must not
43
+ // hand-edit this file — workspace customizations should go into AGENTS.md
44
+ // / SOUL.md / TOOLS.md, which are env-driven and untouched here. Surface a
45
+ // warning when we replace a file whose content diverges, so an operator
46
+ // who did edit it locally gets a signal instead of silently losing changes.
35
47
  const claudeMdPath = path.join(workspaceDir, "CLAUDE.md");
36
- if (!fs.existsSync(claudeMdPath)) {
37
- fs.writeFileSync(claudeMdPath, CLAUDE_MD, "utf8");
48
+ if (log) {
49
+ try {
50
+ const existing = fs.readFileSync(claudeMdPath, "utf8");
51
+ if (existing !== CLAUDE_MD) {
52
+ log.warn(
53
+ `claude-agent: overwriting divergent ${claudeMdPath} with bridge-managed template ` +
54
+ `(local edits to CLAUDE.md are not preserved — customize AGENTS.md / SOUL.md / TOOLS.md instead)`,
55
+ );
56
+ }
57
+ } catch {
58
+ // file missing or unreadable — first-boot case, no warning needed
59
+ }
38
60
  }
61
+ fs.writeFileSync(claudeMdPath, CLAUDE_MD, "utf8");
39
62
  }