@link-assistant/agent 0.5.0 → 0.5.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +249 -209
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -673,97 +673,253 @@ async function main() {
673
673
  .command(McpCommand)
674
674
  // Auth subcommand
675
675
  .command(AuthCommand)
676
- // Default run mode (when piping stdin)
677
- .option('model', {
678
- type: 'string',
679
- description: 'Model to use in format providerID/modelID',
680
- default: 'opencode/grok-code',
681
- })
682
- .option('json-standard', {
683
- type: 'string',
684
- description:
685
- 'JSON output format standard: "opencode" (default) or "claude" (experimental)',
686
- default: 'opencode',
687
- choices: ['opencode', 'claude'],
688
- })
689
- .option('system-message', {
690
- type: 'string',
691
- description: 'Full override of the system message',
692
- })
693
- .option('system-message-file', {
694
- type: 'string',
695
- description: 'Full override of the system message from file',
696
- })
697
- .option('append-system-message', {
698
- type: 'string',
699
- description: 'Append to the default system message',
700
- })
701
- .option('append-system-message-file', {
702
- type: 'string',
703
- description: 'Append to the default system message from file',
704
- })
705
- .option('server', {
706
- type: 'boolean',
707
- description: 'Run in server mode (default)',
708
- default: true,
709
- })
710
- .option('verbose', {
711
- type: 'boolean',
712
- description:
713
- 'Enable verbose mode to debug API requests (shows system prompt, token counts, etc.)',
714
- default: false,
715
- })
716
- .option('dry-run', {
717
- type: 'boolean',
718
- description:
719
- 'Simulate operations without making actual API calls or package installations (useful for testing)',
720
- default: false,
721
- })
722
- .option('use-existing-claude-oauth', {
723
- type: 'boolean',
724
- description:
725
- 'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
726
- default: false,
727
- })
728
- .option('prompt', {
729
- alias: 'p',
730
- type: 'string',
731
- description: 'Prompt message to send directly (bypasses stdin reading)',
732
- })
733
- .option('disable-stdin', {
734
- type: 'boolean',
735
- description:
736
- 'Disable stdin streaming mode (requires --prompt or shows help)',
737
- default: false,
738
- })
739
- .option('stdin-stream-timeout', {
740
- type: 'number',
741
- description:
742
- 'Optional timeout in milliseconds for stdin reading (default: no timeout)',
743
- })
744
- .option('auto-merge-queued-messages', {
745
- type: 'boolean',
746
- description:
747
- 'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
748
- default: true,
749
- })
750
- .option('interactive', {
751
- type: 'boolean',
752
- description:
753
- 'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
754
- default: true,
755
- })
756
- .option('always-accept-stdin', {
757
- type: 'boolean',
758
- description:
759
- 'Keep accepting stdin input even after the agent finishes work (default: true). Use --no-always-accept-stdin for single-message mode.',
760
- default: true,
761
- })
762
- .option('compact-json', {
763
- type: 'boolean',
764
- description:
765
- 'Output compact JSON (single line) instead of pretty-printed JSON (default: false). Useful for program-to-program communication.',
766
- default: false,
676
+ // Default command for agent mode (when no subcommand specified)
677
+ .command({
678
+ command: '$0',
679
+ describe: 'Run agent in interactive or stdin mode (default)',
680
+ builder: (yargs) =>
681
+ yargs
682
+ .option('model', {
683
+ type: 'string',
684
+ description: 'Model to use in format providerID/modelID',
685
+ default: 'opencode/grok-code',
686
+ })
687
+ .option('json-standard', {
688
+ type: 'string',
689
+ description:
690
+ 'JSON output format standard: "opencode" (default) or "claude" (experimental)',
691
+ default: 'opencode',
692
+ choices: ['opencode', 'claude'],
693
+ })
694
+ .option('system-message', {
695
+ type: 'string',
696
+ description: 'Full override of the system message',
697
+ })
698
+ .option('system-message-file', {
699
+ type: 'string',
700
+ description: 'Full override of the system message from file',
701
+ })
702
+ .option('append-system-message', {
703
+ type: 'string',
704
+ description: 'Append to the default system message',
705
+ })
706
+ .option('append-system-message-file', {
707
+ type: 'string',
708
+ description: 'Append to the default system message from file',
709
+ })
710
+ .option('server', {
711
+ type: 'boolean',
712
+ description: 'Run in server mode (default)',
713
+ default: true,
714
+ })
715
+ .option('verbose', {
716
+ type: 'boolean',
717
+ description:
718
+ 'Enable verbose mode to debug API requests (shows system prompt, token counts, etc.)',
719
+ default: false,
720
+ })
721
+ .option('dry-run', {
722
+ type: 'boolean',
723
+ description:
724
+ 'Simulate operations without making actual API calls or package installations (useful for testing)',
725
+ default: false,
726
+ })
727
+ .option('use-existing-claude-oauth', {
728
+ type: 'boolean',
729
+ description:
730
+ 'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
731
+ default: false,
732
+ })
733
+ .option('prompt', {
734
+ alias: 'p',
735
+ type: 'string',
736
+ description:
737
+ 'Prompt message to send directly (bypasses stdin reading)',
738
+ })
739
+ .option('disable-stdin', {
740
+ type: 'boolean',
741
+ description:
742
+ 'Disable stdin streaming mode (requires --prompt or shows help)',
743
+ default: false,
744
+ })
745
+ .option('stdin-stream-timeout', {
746
+ type: 'number',
747
+ description:
748
+ 'Optional timeout in milliseconds for stdin reading (default: no timeout)',
749
+ })
750
+ .option('auto-merge-queued-messages', {
751
+ type: 'boolean',
752
+ description:
753
+ 'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
754
+ default: true,
755
+ })
756
+ .option('interactive', {
757
+ type: 'boolean',
758
+ description:
759
+ 'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
760
+ default: true,
761
+ })
762
+ .option('always-accept-stdin', {
763
+ type: 'boolean',
764
+ description:
765
+ 'Keep accepting stdin input even after the agent finishes work (default: true). Use --no-always-accept-stdin for single-message mode.',
766
+ default: true,
767
+ })
768
+ .option('compact-json', {
769
+ type: 'boolean',
770
+ description:
771
+ 'Output compact JSON (single line) instead of pretty-printed JSON (default: false). Useful for program-to-program communication.',
772
+ default: false,
773
+ }),
774
+ handler: async (argv) => {
775
+ const compactJson = argv['compact-json'] === true;
776
+
777
+ // Check if --prompt flag was provided
778
+ if (argv.prompt) {
779
+ // Direct prompt mode - bypass stdin entirely
780
+ const request = { message: argv.prompt };
781
+ await runAgentMode(argv, request);
782
+ return;
783
+ }
784
+
785
+ // Check if --disable-stdin was set without --prompt
786
+ if (argv['disable-stdin']) {
787
+ // Output a helpful message suggesting to use --prompt
788
+ outputStatus(
789
+ {
790
+ type: 'error',
791
+ message:
792
+ 'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
793
+ hint: 'Example: agent -p "Hello, how are you?"',
794
+ },
795
+ compactJson
796
+ );
797
+ process.exit(1);
798
+ }
799
+
800
+ // Check if stdin is a TTY (interactive terminal)
801
+ if (process.stdin.isTTY) {
802
+ // Enter interactive terminal mode with continuous listening
803
+ const isInteractive = argv.interactive !== false;
804
+ const autoMerge = argv['auto-merge-queued-messages'] !== false;
805
+ const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
806
+
807
+ // Exit if --no-always-accept-stdin is set (single message mode not supported in TTY)
808
+ if (!alwaysAcceptStdin) {
809
+ outputStatus(
810
+ {
811
+ type: 'error',
812
+ message:
813
+ 'Single message mode (--no-always-accept-stdin) is not supported in interactive terminal mode.',
814
+ hint: 'Use piped input or --prompt for single messages.',
815
+ },
816
+ compactJson
817
+ );
818
+ process.exit(1);
819
+ }
820
+
821
+ outputStatus(
822
+ {
823
+ type: 'status',
824
+ mode: 'interactive-terminal',
825
+ message:
826
+ 'Agent CLI in interactive terminal mode. Type your message and press Enter.',
827
+ hint: 'Press CTRL+C to exit. Use --help for options.',
828
+ acceptedFormats: isInteractive
829
+ ? ['JSON object with "message" field', 'Plain text']
830
+ : ['JSON object with "message" field'],
831
+ options: {
832
+ interactive: isInteractive,
833
+ autoMergeQueuedMessages: autoMerge,
834
+ alwaysAcceptStdin,
835
+ compactJson,
836
+ },
837
+ },
838
+ compactJson
839
+ );
840
+
841
+ // Use continuous mode for interactive terminal
842
+ await runContinuousAgentMode(argv);
843
+ return;
844
+ }
845
+
846
+ // stdin is piped - enter stdin listening mode
847
+ const isInteractive = argv.interactive !== false;
848
+ const autoMerge = argv['auto-merge-queued-messages'] !== false;
849
+ const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
850
+
851
+ outputStatus(
852
+ {
853
+ type: 'status',
854
+ mode: 'stdin-stream',
855
+ message: alwaysAcceptStdin
856
+ ? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
857
+ : 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
858
+ hint: 'Press CTRL+C to exit. Use --help for options.',
859
+ acceptedFormats: isInteractive
860
+ ? ['JSON object with "message" field', 'Plain text']
861
+ : ['JSON object with "message" field'],
862
+ options: {
863
+ interactive: isInteractive,
864
+ autoMergeQueuedMessages: autoMerge,
865
+ alwaysAcceptStdin,
866
+ compactJson,
867
+ },
868
+ },
869
+ compactJson
870
+ );
871
+
872
+ // Use continuous mode if --always-accept-stdin is enabled (default)
873
+ if (alwaysAcceptStdin) {
874
+ await runContinuousAgentMode(argv);
875
+ return;
876
+ }
877
+
878
+ // Single-message mode (--no-always-accept-stdin)
879
+ const timeout = argv['stdin-stream-timeout'] ?? null;
880
+ const input = await readStdinWithTimeout(timeout);
881
+ const trimmedInput = input.trim();
882
+
883
+ if (trimmedInput === '') {
884
+ outputStatus(
885
+ {
886
+ type: 'status',
887
+ message: 'No input received. Exiting.',
888
+ },
889
+ compactJson
890
+ );
891
+ yargsInstance.showHelp();
892
+ process.exit(0);
893
+ }
894
+
895
+ // Try to parse as JSON, if it fails treat it as plain text message
896
+ let request;
897
+ try {
898
+ request = JSON.parse(trimmedInput);
899
+ } catch (_e) {
900
+ // Not JSON
901
+ if (!isInteractive) {
902
+ // In non-interactive mode, only accept JSON
903
+ outputStatus(
904
+ {
905
+ type: 'error',
906
+ message:
907
+ 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
908
+ hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
909
+ },
910
+ compactJson
911
+ );
912
+ process.exit(1);
913
+ }
914
+ // In interactive mode, treat as plain text message
915
+ request = {
916
+ message: trimmedInput,
917
+ };
918
+ }
919
+
920
+ // Run agent mode
921
+ await runAgentMode(argv, request);
922
+ },
767
923
  })
768
924
  // Initialize logging early for all CLI commands
769
925
  // This prevents debug output from appearing in CLI unless --verbose is used
@@ -815,124 +971,8 @@ async function main() {
815
971
  })
816
972
  .help();
817
973
 
818
- const argv = await yargsInstance.argv;
819
-
820
- // If a command was executed (like mcp), yargs handles it
821
- // Otherwise, check if we should run in agent mode (stdin piped)
822
- const commandExecuted = argv._ && argv._.length > 0;
823
-
824
- if (!commandExecuted) {
825
- const compactJson = argv['compact-json'] === true;
826
-
827
- // Check if --prompt flag was provided
828
- if (argv.prompt) {
829
- // Direct prompt mode - bypass stdin entirely
830
- const request = { message: argv.prompt };
831
- await runAgentMode(argv, request);
832
- return;
833
- }
834
-
835
- // Check if --disable-stdin was set without --prompt
836
- if (argv['disable-stdin']) {
837
- // Output a helpful message suggesting to use --prompt
838
- outputStatus(
839
- {
840
- type: 'error',
841
- message:
842
- 'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
843
- hint: 'Example: agent -p "Hello, how are you?"',
844
- },
845
- compactJson
846
- );
847
- process.exit(1);
848
- }
849
-
850
- // Check if stdin is a TTY (interactive terminal)
851
- // If it is, show help instead of waiting for input
852
- if (process.stdin.isTTY) {
853
- yargsInstance.showHelp();
854
- process.exit(0);
855
- }
856
-
857
- // stdin is piped - enter stdin listening mode
858
- // Output status message to inform user what's happening
859
- const isInteractive = argv.interactive !== false;
860
- const autoMerge = argv['auto-merge-queued-messages'] !== false;
861
- const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
862
-
863
- outputStatus(
864
- {
865
- type: 'status',
866
- mode: 'stdin-stream',
867
- message: alwaysAcceptStdin
868
- ? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
869
- : 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
870
- hint: 'Press CTRL+C to exit. Use --help for options.',
871
- acceptedFormats: isInteractive
872
- ? ['JSON object with "message" field', 'Plain text']
873
- : ['JSON object with "message" field'],
874
- options: {
875
- interactive: isInteractive,
876
- autoMergeQueuedMessages: autoMerge,
877
- alwaysAcceptStdin,
878
- compactJson,
879
- },
880
- },
881
- compactJson
882
- );
883
-
884
- // Use continuous mode if --always-accept-stdin is enabled (default)
885
- if (alwaysAcceptStdin) {
886
- await runContinuousAgentMode(argv);
887
- return;
888
- }
889
-
890
- // Single-message mode (--no-always-accept-stdin)
891
- // Read stdin with optional timeout
892
- const timeout = argv['stdin-stream-timeout'] ?? null;
893
- const input = await readStdinWithTimeout(timeout);
894
- const trimmedInput = input.trim();
895
-
896
- if (trimmedInput === '') {
897
- outputStatus(
898
- {
899
- type: 'status',
900
- message: 'No input received. Exiting.',
901
- },
902
- compactJson
903
- );
904
- yargsInstance.showHelp();
905
- process.exit(0);
906
- }
907
-
908
- // Try to parse as JSON, if it fails treat it as plain text message
909
- let request;
910
- try {
911
- request = JSON.parse(trimmedInput);
912
- } catch (_e) {
913
- // Not JSON
914
- if (!isInteractive) {
915
- // In non-interactive mode, only accept JSON
916
- outputStatus(
917
- {
918
- type: 'error',
919
- message:
920
- 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
921
- hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
922
- },
923
- compactJson
924
- );
925
- process.exit(1);
926
- }
927
- // In interactive mode, treat as plain text message
928
- request = {
929
- message: trimmedInput,
930
- };
931
- }
932
-
933
- // Run agent mode
934
- await runAgentMode(argv, request);
935
- }
974
+ // Parse arguments (handlers will be called automatically)
975
+ await yargsInstance.argv;
936
976
  } catch (error) {
937
977
  hasError = true;
938
978
  console.error(