@link-assistant/agent 0.22.10 → 0.24.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.
@@ -52,6 +52,10 @@ export interface AgentConfig {
52
52
  mcpDefaultToolCallTimeout: number;
53
53
  mcpMaxToolCallTimeout: number;
54
54
  verifyImagesAtReadTool: boolean;
55
+ readOnly: boolean;
56
+ disableTools: string;
57
+ permissionMode: string;
58
+ permission: string;
55
59
  }
56
60
 
57
61
  // Fallback helpers for when config is not yet initialized (early imports/tests)
@@ -132,6 +136,10 @@ function defaultConfig(): AgentConfig {
132
136
  })(),
133
137
  verifyImagesAtReadTool:
134
138
  getEnvStr('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL') !== 'false',
139
+ readOnly: truthyEnv('LINK_ASSISTANT_AGENT_READ_ONLY'),
140
+ disableTools: getEnvStr('LINK_ASSISTANT_AGENT_DISABLE_TOOLS') ?? '',
141
+ permissionMode: getEnvStr('LINK_ASSISTANT_AGENT_PERMISSION_MODE') ?? 'auto',
142
+ permission: getEnvStr('LINK_ASSISTANT_AGENT_PERMISSION') ?? '',
135
143
  };
136
144
  }
137
145
 
@@ -240,6 +248,31 @@ function buildAgentConfigOptions({
240
248
  type: 'boolean',
241
249
  description: 'Verify images when using the read tool',
242
250
  default: getenv('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL', true),
251
+ })
252
+ .option('read-only', {
253
+ type: 'boolean',
254
+ description:
255
+ 'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch).',
256
+ default: getenv('LINK_ASSISTANT_AGENT_READ_ONLY', false),
257
+ })
258
+ .option('disable-tools', {
259
+ type: 'string',
260
+ description:
261
+ 'Comma-separated list of tool ids to disable (e.g. "bash,write,edit"). Disabled tools are never exposed to the model and are rejected if invoked via batch.',
262
+ default: getenv('LINK_ASSISTANT_AGENT_DISABLE_TOOLS', ''),
263
+ })
264
+ .option('permission-mode', {
265
+ type: 'string',
266
+ choices: ['auto', 'plan', 'readonly', 'ask'],
267
+ description:
268
+ 'Permission policy for mutating tools. "auto" (default) allows everything; "plan" allows read-only shell and asks before mutations; "readonly" denies all mutations; "ask" asks before every mutating tool. Approvals are driven over JSON (permission_request / permission_response).',
269
+ default: getenv('LINK_ASSISTANT_AGENT_PERMISSION_MODE', 'auto'),
270
+ })
271
+ .option('permission', {
272
+ type: 'string',
273
+ description:
274
+ 'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
275
+ default: getenv('LINK_ASSISTANT_AGENT_PERMISSION', ''),
243
276
  });
244
277
  }
245
278
 
@@ -292,6 +325,10 @@ export function initConfig(argv?: string[]): AgentConfig {
292
325
  mcpDefaultToolCallTimeout: parsed.mcpDefaultToolCallTimeout ?? 120000,
293
326
  mcpMaxToolCallTimeout: parsed.mcpMaxToolCallTimeout ?? 600000,
294
327
  verifyImagesAtReadTool: parsed.verifyImagesAtReadTool ?? true,
328
+ readOnly: parsed.readOnly ?? false,
329
+ disableTools: parsed.disableTools ?? '',
330
+ permissionMode: parsed.permissionMode ?? 'auto',
331
+ permission: parsed.permission ?? '',
295
332
  });
296
333
 
297
334
  // Propagate verbose to env var for subprocess resilience (issue #227).
package/src/index.js CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  runContinuousDirectMode,
37
37
  resolveResumeSession,
38
38
  } from './cli/continuous-mode.js';
39
+ import { parseStreamJsonInput } from './cli/input-queue.js';
39
40
  import { createBusEventSubscription } from './cli/event-handler.js';
40
41
  import {
41
42
  outputStatus,
@@ -259,6 +260,19 @@ async function readSystemMessages(argv) {
259
260
  return { systemMessage, appendSystemMessage };
260
261
  }
261
262
 
263
+ function getInputFormat(argv) {
264
+ return argv['input-format'] || argv.inputFormat || 'text';
265
+ }
266
+
267
+ function getAcceptedInputFormats(inputFormat, isInteractive) {
268
+ if (inputFormat === 'stream-json') {
269
+ return ['Claude stream-json user frames'];
270
+ }
271
+ return isInteractive
272
+ ? ['JSON object with "message" field', 'Plain text']
273
+ : ['JSON object with "message" field'];
274
+ }
275
+
262
276
  async function runAgentMode(argv, request) {
263
277
  // Log version and command info in verbose mode using lazy logging
264
278
  Log.Default.info(() => ({
@@ -663,8 +677,15 @@ async function main() {
663
677
  // Check if stdin is a TTY (interactive terminal)
664
678
  if (process.stdin.isTTY) {
665
679
  // Enter interactive terminal mode with continuous listening
666
- const isInteractive = argv.interactive !== false;
667
- const autoMerge = argv['auto-merge-queued-messages'] !== false;
680
+ const inputFormat = getInputFormat(argv);
681
+ const isInteractive =
682
+ inputFormat === 'stream-json'
683
+ ? false
684
+ : argv.interactive !== false;
685
+ const autoMerge =
686
+ inputFormat === 'stream-json'
687
+ ? false
688
+ : argv['auto-merge-queued-messages'] !== false;
668
689
  const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
669
690
 
670
691
  // Exit if --no-always-accept-stdin is set (single message mode not supported in TTY)
@@ -688,10 +709,12 @@ async function main() {
688
709
  message:
689
710
  'Agent CLI in interactive terminal mode. Type your message and press Enter.',
690
711
  hint: 'Press CTRL+C to exit. Use --help for options.',
691
- acceptedFormats: isInteractive
692
- ? ['JSON object with "message" field', 'Plain text']
693
- : ['JSON object with "message" field'],
712
+ acceptedFormats: getAcceptedInputFormats(
713
+ inputFormat,
714
+ isInteractive
715
+ ),
694
716
  options: {
717
+ inputFormat,
695
718
  interactive: isInteractive,
696
719
  autoMergeQueuedMessages: autoMerge,
697
720
  alwaysAcceptStdin,
@@ -707,8 +730,13 @@ async function main() {
707
730
  }
708
731
 
709
732
  // stdin is piped - enter stdin listening mode
710
- const isInteractive = argv.interactive !== false;
711
- const autoMerge = argv['auto-merge-queued-messages'] !== false;
733
+ const inputFormat = getInputFormat(argv);
734
+ const isInteractive =
735
+ inputFormat === 'stream-json' ? false : argv.interactive !== false;
736
+ const autoMerge =
737
+ inputFormat === 'stream-json'
738
+ ? false
739
+ : argv['auto-merge-queued-messages'] !== false;
712
740
  const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
713
741
 
714
742
  outputStatus(
@@ -719,10 +747,12 @@ async function main() {
719
747
  ? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
720
748
  : 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
721
749
  hint: 'Press CTRL+C to exit. Use --help for options.',
722
- acceptedFormats: isInteractive
723
- ? ['JSON object with "message" field', 'Plain text']
724
- : ['JSON object with "message" field'],
750
+ acceptedFormats: getAcceptedInputFormats(
751
+ inputFormat,
752
+ isInteractive
753
+ ),
725
754
  options: {
755
+ inputFormat,
726
756
  interactive: isInteractive,
727
757
  autoMergeQueuedMessages: autoMerge,
728
758
  alwaysAcceptStdin,
@@ -757,27 +787,52 @@ async function main() {
757
787
 
758
788
  // Try to parse as JSON, if it fails treat it as plain text message
759
789
  let request;
760
- try {
761
- request = JSON.parse(trimmedInput);
762
- } catch (_e) {
763
- // Not JSON
764
- if (!isInteractive) {
765
- // In non-interactive mode, only accept JSON
790
+ if (inputFormat === 'stream-json') {
791
+ try {
792
+ const firstFrame = trimmedInput
793
+ .split(/\r?\n/)
794
+ .find((line) => line.trim());
795
+ request = parseStreamJsonInput(firstFrame || '');
796
+ if (request.kind !== 'message') {
797
+ throw new Error(
798
+ `Expected stream-json user frame, received ${request.kind}`
799
+ );
800
+ }
801
+ } catch (error) {
766
802
  outputError(
767
803
  {
768
804
  errorType: 'ValidationError',
769
805
  message:
770
- 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
771
- hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
806
+ error instanceof Error ? error.message : String(error),
807
+ hint: 'Provide one Claude stream-json user frame per line.',
772
808
  },
773
809
  compactJson
774
810
  );
775
811
  process.exit(1);
776
812
  }
777
- // In interactive mode, treat as plain text message
778
- request = {
779
- message: trimmedInput,
780
- };
813
+ } else {
814
+ try {
815
+ request = JSON.parse(trimmedInput);
816
+ } catch (_e) {
817
+ // Not JSON
818
+ if (!isInteractive) {
819
+ // In non-interactive mode, only accept JSON
820
+ outputError(
821
+ {
822
+ errorType: 'ValidationError',
823
+ message:
824
+ 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
825
+ hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
826
+ },
827
+ compactJson
828
+ );
829
+ process.exit(1);
830
+ }
831
+ // In interactive mode, treat as plain text message
832
+ request = {
833
+ message: trimmedInput,
834
+ };
835
+ }
781
836
  }
782
837
 
783
838
  // Output input confirmation in JSON format
@@ -785,7 +840,12 @@ async function main() {
785
840
  {
786
841
  raw: trimmedInput,
787
842
  parsed: request,
788
- format: isInteractive ? 'text' : 'json',
843
+ format:
844
+ inputFormat === 'stream-json'
845
+ ? 'stream-json'
846
+ : isInteractive
847
+ ? 'text'
848
+ : 'json',
789
849
  },
790
850
  compactJson
791
851
  );